Mutations (useMutation)
How to use
Basic mutation
"use client";import { useMutation, queryAtom } from "@mongez/atomic-query";
function CreateUserForm() { const createUser = useMutation<User, { name: string }>({ mutationFn: async ({ name }, { signal }) => fetch("/api/users", { method: "POST", body: JSON.stringify({ name }), signal, }).then(r => r.json()),
onSuccess: (created) => { // Append the new record to the list cache queryAtom.updateQueryData<User[]>(["users"], old => [...(old ?? []), created] ); },
onSettled: () => { // Refresh any derived queries regardless of outcome queryAtom.invalidate({ queryKey: ["users", "stats"] }); }, });
return ( <button disabled={createUser.isPending} onClick={() => createUser.mutate({ name: "Alice" })} > {createUser.isPending ? "Creating…" : "Create user"} </button> );}Full options
useMutation<TData, TVariables, TContext>({ mutationFn: (variables: TVariables, ctx: { signal: AbortSignal }) => Promise<TData>;
// Runs BEFORE the mutation fires. Return value becomes `context` in onError. onMutate?: (variables: TVariables) => TContext | Promise<TContext>;
// Called on success. onSuccess?: (data: TData, variables: TVariables, context: TContext | undefined) => void | Promise<void>;
// Called on failure. Use `context` to roll back optimistic state. onError?: (error: unknown, variables: TVariables, context: TContext | undefined) => void | Promise<void>;
// Called after success or error (always fires). onSettled?: ( data: TData | undefined, error: unknown, variables: TVariables, context: TContext | undefined, ) => void | Promise<void>;})Return shape
| Field | Type | Meaning |
|---|---|---|
mutate | (variables) => Promise<TData> | Fire the mutation; returns a promise. |
mutateAsync | (variables) => Promise<TData> | Alias for mutate. |
reset | () => void | Clear state and abort any in-flight call. |
status | "idle" | "pending" | "error" | "success" | Current lifecycle state. |
isPending | boolean | Mutation is in flight. |
isError | boolean | Last call failed. |
isSuccess | boolean | Last call succeeded. |
isIdle | boolean | Never fired, or after reset(). |
data | TData | undefined | Result of the last successful call. |
error | unknown | Error from the last failed call. |
variables | TVariables | undefined | Variables passed to the last call. |
Optimistic update with rollback
const updateUser = useMutation<User, { id: number; name: string }, { previous: User[] | undefined }>({ // 1. Snapshot the cache and apply the optimistic change before the request fires. onMutate: async ({ id, name }) => { const previous = queryAtom.getData(["users"]) as User[] | undefined;
queryAtom.updateQueryData<User[]>(["users"], old => (old ?? []).map(u => u.id === id ? { ...u, name } : u) );
return { previous }; // This becomes `context` in onError },
mutationFn: async ({ id, name }, { signal }) => fetch(`/api/users/${id}`, { method: "PATCH", body: JSON.stringify({ name }), signal, }).then(r => r.json()),
// 2. On error, restore the snapshot. onError: (_err, _vars, context) => { if (context?.previous !== undefined) { queryAtom.updateQueryData(["users"], () => context.previous!); } },
// 3. On success, replace the optimistic stub with the server's canonical record. onSuccess: (serverUser) => { queryAtom.updateQueryData<User[]>(["users"], old => (old ?? []).map(u => u.id === serverUser.id ? serverUser : u) ); },});Direct cache write without a mutation hook
When you only need to update the cache without a side effect:
// AppendqueryAtom.updateQueryData<User[]>(["users"], old => [...(old ?? []), newUser]);
// Remove by idqueryAtom.updateQueryData<User[]>(["users"], old => (old ?? []).filter(u => u.id !== deletedId));
// Replace one recordqueryAtom.updateQueryData<User[]>(["users"], old => (old ?? []).map(u => u.id === updated.id ? updated : u));Standalone export:
import { updateQueryData } from "@mongez/atomic-query";Key details / Pitfalls
-
Mutations do NOT write to the
queryAtomcache themselves. The hook tracks its own localstatus/data/error. Cache interaction is always explicit: you callqueryAtom.updateQueryData,queryAtom.push,queryAtom.invalidate, etc. inside the callbacks. -
A second
mutatecall automatically aborts the previous in-flight one. There is no built-in debounce; if you need it, add your own. Thereset()method also aborts the current in-flight call. -
Unmounting aborts the in-flight mutation. The
onSuccess/onError/onSettledcallbacks are NOT called when the signal is aborted (the hook checkscontroller.signal.abortedbefore invoking them). -
onMutateruns beforemutationFn, even before the network request starts. IfonMutatethrows,mutationFnis never called andonErrorfires with theonMutateerror. -
updateQueryDatais a no-op when the query does not exist yet. If you call it for a key that has never been loaded, it silently does nothing. Pre-populate withseedQueryif you need it to work before the firstuseQuerymount. -
mutatevsmutateAsync: They are the same function. Both return a promise and throw on failure. The distinction from TanStack Query (wheremutateswallowed errors) does not apply here — always wrap in try/catch or.catch()when using the return value.