useM
useM is the mutation hook. It wraps useMutation with route-aware typing, automatic tag invalidation, and a structured optimistic-update API.
const createPost = useM("createPost", { pathParams: { facilityId } });
createPost.mutate({ body: { title: "Hello", body: "World" } });Signature
Section titled “Signature”function useM<TRoute extends MutatingRouteKey<TSchema>>( routeName: TRoute, options: UseMOptions<TSchema, TRoute, TParsedError>,): UseMutationResult<…>;Path params are passed up-front in options.pathParams because they’re typically static per render. The body (and per-call search params, headers, etc.) are passed when you call mutate.
Basic mutation
Section titled “Basic mutation”function NewPost({ facilityId }: { facilityId: string }) { const createPost = useM("createPost", { pathParams: { facilityId } });
return ( <form onSubmit={(e) => { e.preventDefault(); const data = new FormData(e.currentTarget); createPost.mutate({ body: { title: String(data.get("title")), body: String(data.get("body")), }, }); }} > <input name="title" required /> <textarea name="body" required /> <button disabled={createPost.isPending}>Publish</button> {createPost.isError && <p>{createPost.error.message}</p>} </form> );}Because the route’s invalidatesTags chain into matching tags, any useQ("listPosts", …) for the same facilityId refetches automatically after the mutation settles.
Optimistic updates
Section titled “Optimistic updates”useM accepts an optimistic array. Each entry says: “before the request goes out, update this cached query as if the mutation already succeeded.” If the request fails, every snapshot rolls back.
Single target
Section titled “Single target”const updatePost = useM("updatePost", { pathParams: { facilityId, postId }, optimistic: [ { route: "getPost", params: { pathParams: { facilityId, postId } }, updater: (prev, vars) => prev && { ...prev, ...vars.body }, }, ],});The updater receives the previous cached value and the variables passed to mutate. Return the next value (or prev to do nothing).
Multiple targets
Section titled “Multiple targets”A mutation often touches several caches. Each OptimisticTarget is applied independently with its own snapshot:
const updatePost = useM("updatePost", { pathParams: { facilityId, postId }, optimistic: [ { // Detail page route: "getPost", params: { pathParams: { facilityId, postId } }, updater: (prev, vars) => prev && { ...prev, ...vars.body }, }, { // List view route: "listPosts", params: { pathParams: { facilityId } }, updater: (prev, vars) => prev?.map((p) => (p.id === postId ? { ...p, ...vars.body } : p)), }, ],});List insert / delete
Section titled “List insert / delete”const createPost = useM("createPost", { pathParams: { facilityId }, optimistic: [ { route: "listPosts", params: { pathParams: { facilityId } }, updater: (prev, vars) => [ { id: `temp-${crypto.randomUUID()}`, facilityId, createdAt: new Date().toISOString(), ...vars.body, }, ...(prev ?? []), ], }, ],});
const deletePost = useM("deletePost", { pathParams: { facilityId, postId }, optimistic: [ { route: "listPosts", params: { pathParams: { facilityId } }, updater: (prev) => prev?.filter((p) => p.id !== postId), }, ],});Rollback semantics
Section titled “Rollback semantics”Under the hood, useM does this for every OptimisticTarget:
cancelQueries(key)to stop any in-flight refetch from clobbering the optimistic value.getQueryData(key)to snapshot the current state.setQueryData(key, updater(prev, vars))to apply the optimistic update.- On
onError, restore each snapshot in reverse order. - On
onSettled, run tag invalidation so any drift is reconciled.
You see this in useM.context:
createPost.mutate(vars, { onError: (err, vars, ctx) => { // ctx is the array of snapshots; useM already used it for rollback. console.error("rolled back", ctx?.snapshots?.length, "snapshots"); },});additionalInvalidatesTags
Section titled “additionalInvalidatesTags”Sometimes a mutation needs to invalidate caches that aren’t in the schema’s invalidatesTags (e.g. cross-cutting “feed” views). Add them at the call site:
const createPost = useM("createPost", { pathParams: { facilityId }, additionalInvalidatesTags: [ { type: "feed" }, { type: "post", facilityId: () => "everywhere" }, ],});additionalInvalidatesTags are unioned with the schema’s invalidatesTags. Both run in onSettled.
Per-call options
Section titled “Per-call options”mutate accepts the standard TanStack Query options:
createPost.mutate( { body: { title, body } }, { onSuccess: (post) => router.push(`/posts/${post.id}`), onError: (err) => toast.error(err.message), onSettled: () => analytics.track("post_create_attempt"), },);These run in addition to the ones declared at the hook level — useful for one-off behaviors.
Tag chaining recap
Section titled “Tag chaining recap”| Where you declare it | What it does |
|---|---|
Route tags (in schema, on a read route) | Labels the cache entry so mutations can find it. |
Route invalidatesTags (in schema, on a write route) | Runs invalidateQueries for matching tags in onSettled. |
useM additionalInvalidatesTags (call-site) | Extra tags to invalidate for this particular hook. |
The matching algorithm:
- Dynamic tag
{ type: "post", facilityId: "f1" }matches read tags with sametypeand equal-or-missingfacilityId. - Static tag
{ type: "post" }matches everything withtype: "post", ignoring other keys.
See Tag invalidation for the deep dive.