Mutations
Mutations are for writes — create, update, delete. Unlike queries they run on demand via mutate(), are never cached by key, and never refetch on their own.
Live Example
Basic Mutation
import { MutationObserver } from "@domphy/query"
import { toState } from "@domphy/core"
const saving = toState(false)
const mutation = new MutationObserver(queryClient, {
mutationFn: (todo: { title: string }) =>
fetch("/api/todos", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(todo),
}).then((res) => res.json()),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ["todos"] }),
})
mutation.subscribe((result) => saving.set(result.isPending))
Trigger it from an event handler:
{
button: (l) => (saving.get(l) ? "Saving..." : "Save"),
$: [button({ color: "primary" })],
ariaDisabled: (l) => saving.get(l),
onClick: () => mutation.mutate({ title: "New todo" }).catch(() => undefined),
}
mutate() returns a promise that rejects on failure — handle it (or read result.error from the subscription instead).
Lifecycle Callbacks
new MutationObserver(queryClient, {
mutationFn: updateTodo,
onMutate: (variables) => {
// runs before mutationFn; the return value becomes `context`
return { startedAt: performance.now() }
},
onSuccess: (data, variables, context) => {},
onError: (error, variables, context) => {},
onSettled: (data, error, variables, context) => {
// success or error — the usual place to invalidate
queryClient.invalidateQueries({ queryKey: ["todos"] })
},
})
Optimistic Updates
Update the cache immediately in onMutate, roll back from context on error:
new MutationObserver(queryClient, {
mutationFn: updateTodo,
onMutate: async (newTodo) => {
// stop in-flight refetches from overwriting the optimistic value
await queryClient.cancelQueries({ queryKey: ["todos"] })
const previous = queryClient.getQueryData<Todo[]>(["todos"])
queryClient.setQueryData<Todo[]>(["todos"], (old) =>
(old ?? []).map((todo) => (todo.id === newTodo.id ? newTodo : todo)),
)
return { previous }
},
onError: (error, newTodo, context) => {
queryClient.setQueryData(["todos"], context?.previous)
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ["todos"] })
},
})
Because the cache update notifies every QueryObserver on ["todos"], the optimistic value flows through the normal bridge into toState — the UI updates instantly with no extra wiring.
Mutation Result
The subscription result mirrors queries:
status:"idle" | "pending" | "error" | "success"isIdle,isPending,isError,isSuccessdata,error,variables,failureCountmutate(variables),reset()
Retry
Mutations do not retry by default (writes are not safely repeatable in general). Opt in per mutation:
new MutationObserver(queryClient, {
mutationFn: sendTelemetry,
retry: 3,
retryDelay: (attempt) => Math.min(1000 * 2 ** attempt, 30_000),
})