Skip to main content

Queries & Mutations

mobx-chunk includes a built-in data fetching layer — queries, mutations, and infinite queries — with per-argument caching, automatic loading/error tracking, and manual invalidation. Think of it as a simplified TanStack Query powered by MobX observables.

Overview

Define queries directly in your chunk config alongside state, actions, and views:

import { createChunk } from "mobx-chunk";

const usersStore = createChunk({
name: "users",
initialState: { filter: "" },
queries: (self) => ({
fetchUsers: {
fn: async (page: number) => api.getUsers(page, self.filter),
type: "query" as const,
cacheTime: 5000,
},
createUser: {
fn: async (name: string) => api.createUser(name),
type: "mutation" as const,
},
usersFeed: {
fn: async (cursor: number) => api.getUsersFeed(cursor),
type: "infiniteQuery" as const,
initialPageParam: 0,
getNextPageParam: (lastPage) => lastPage.nextCursor,
},
}),
});

Three Definition Types

query — Cached GET requests

Each unique combination of arguments gets its own cache entry with a configurable cacheTime (default 30 seconds). Subsequent calls with the same arguments return cached data instantly.

fetchUsers: {
fn: async (page: number) => api.getUsers(page),
type: "query" as const,
cacheTime: 10000, // 10 seconds
}

Methods on store.queries.fetchUsers:

MethodDescription
(...args)Returns cached data or fetches. Deduplicates concurrent calls with the same args.
.getState(...args)Observable state for the given args: data, error, status, isPending, isError, isSuccess, fetchedAt.
.refetch(...args)Force a fresh fetch, ignoring cache.
.invalidate(...args)Remove cache entry for specific args.
.invalidateAll()Clear entire cache for this query.

mutation — POST/PUT/DELETE (no cache)

Mutations don't cache — each call executes immediately. A single observable state tracks the last call.

createUser: {
fn: async (name: string) => api.createUser(name),
type: "mutation" as const,
}

Methods on store.queries.createUser:

MethodDescription
(...args)Execute the mutation, returns a Promise.
.stateObservable state of the last call: data, error, status, isPending, isError, isSuccess.
.reset()Reset state back to idle.

infiniteQuery — Infinite scroll / load more

Accumulates data across pages. You provide an initialPageParam and a getNextPageParam function that extracts the next page parameter from the last response.

usersFeed: {
fn: async (cursor: number) => api.getUsersFeed(cursor),
type: "infiniteQuery" as const,
initialPageParam: 0,
getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
}

Methods on store.queries.usersFeed:

MethodDescription
()Fetch the first page (or return cache).
.getState()Observable state: pages[], pageParams[], hasNextPage, isFetchingNextPage, isPending, isError.
.fetchNextPage()Load the next page using getNextPageParam from the last response.
.refetch()Clear all pages and reload from initialPageParam.
.invalidate()Reset state to idle and clear pages.

Reading State

In React — use hooks

import { useQuery, useMutation, useInfiniteQuery } from "mobx-chunk";

function UsersList({ page }: { page: number }) {
const { data, isPending, isError, error, refetch } = useQuery(
usersStore.queries.fetchUsers, page
);

if (isPending) return <p>Loading...</p>;
if (isError) return <p>Error: {String(error)}</p>;

return (
<ul>
{data?.map((u) => <li key={u.id}>{u.name}</li>)}
</ul>
);
}

See useQuery, useMutation, useInfiniteQuery API docs.

In MobX code — use getState

Inside actions, views, or reactions, MobX automatically tracks observable access — no hook needed:

// In a computed view
views: (self) => ({
isReady: () => !usersStore.queries.fetchUsers.getState(1).isPending,
})

// In a reaction
import { reaction } from "mobx";
reaction(
() => usersStore.queries.fetchUsers.getState(page).data,
(users) => console.log("Users updated:", users)
);

Splitting Queries with combineQueries

When a chunk has many queries, split them into separate files and merge with combineQueries:

// queries/userQueries.ts
export const userQueries = (self) => ({
fetchUsers: { fn: async (page: number) => api.getUsers(page), type: "query" as const },
createUser: { fn: async (name: string) => api.createUser(name), type: "mutation" as const },
});

// queries/postQueries.ts
export const postQueries = (self) => ({
fetchPosts: { fn: async (userId: number) => api.getPosts(userId), type: "query" as const },
});

// store.ts
import { createChunk, combineQueries } from "mobx-chunk";

const store = createChunk({
name: "app",
initialState: {},
queries: combineQueries({ users: userQueries, posts: postQueries }),
});

// All queries available at one level
await store.queries.fetchUsers(1);
await store.queries.fetchPosts(42);

See combineQueries API docs.

Lifecycle

All query caches live inside the chunk instance and are automatically cleaned up when you call store.dispose().