Mutations
A mutation models a single side effect (POST / PUT / DELETE) and exposes the result + state as effector stores.
Basic usage
import { createMutation } from '@effector-tanstack-query/core'
const addTodo = createMutation({ name: 'addTodo', mutationFn: (text: string) => fetch('/api/todos', { method: 'POST', body: JSON.stringify({ text }), }).then((r) => r.json()), onSuccess: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),})
addTodo.start() // subscribe the observer (call once)addTodo.mutate('Buy milk') // trigger the mutationaddTodo.$status flows: 'idle' → 'pending' → 'success' | 'error'.
Reacting to outcome with sample
For module-level side effects, use the finished events. Payload includes both params (the variables passed to mutate) and result / error.
import { sample } from 'effector'
sample({ clock: addTodo.finished.success, fn: ({ params, result }) => `Added "${params}" → ${result.id}`, target: showToast,})
sample({ clock: addTodo.finished.failure, fn: ({ error }) => error.message, target: showError,})This is the idiomatic effector pattern — keeps reactions declarative and out of components.
Invalidating queries after a mutation
A common reaction is “after this mutation succeeds, refetch that query”. createInvalidate is a small factory that builds the right event for it — it pulls the per-scope QueryClient via $queryClient (so it’s fork-safe) and calls invalidateQueries. Just sample from finished.success into it:
import { createInvalidate, createMutation } from '@effector-tanstack-query/core'import { sample } from 'effector'
const addTodo = createMutation({ name: 'addTodo', mutationFn: postTodo,})
const invalidateTodos = createInvalidate({ queryKey: ['todos'] })
sample({ clock: addTodo.finished.success, target: invalidateTodos,})The same factory supports reactive keys (e.g. queryKey: ['user', $userId]) and key-prefix invalidation (exact: false). See createInvalidate for the full API.
Per-call callbacks
When you need a component-local reaction (e.g. navigate after a button click), use mutateWith instead of mutate:
addTodo.mutateWith({ variables: 'Buy groceries', onSuccess: (data) => navigate(`/todos/${data.id}`), onError: (error) => alert(error.message),})Per-call callbacks fire in addition to observer-level ones (onSuccess in createMutation’s options) — never instead.
onMutate context
onMutate runs before the mutationFn and can return a context that flows to onSuccess, onError, and onSettled:
const updateUser = createMutation<User, Error, User, { snapshot: User }>({ name: 'updateUser', mutationFn: putUser, onMutate: (newUser) => { const snapshot = queryClient.getQueryData(['user', newUser.id]) as User queryClient.setQueryData(['user', newUser.id], newUser) // optimistic update return { snapshot } }, onError: (_err, _vars, context) => { if (context) queryClient.setQueryData(['user', context.snapshot.id], context.snapshot) }, onSettled: () => queryClient.invalidateQueries({ queryKey: ['user'] }),})The 4th generic TOnMutateResult types the context throughout.
Reset
addTodo.reset() // back to idle, clears $data and $errorOffline behavior
When the network is offline (per onlineManager.isOnline()), mutations don’t run — they’re paused. $status becomes 'pending' and $isPaused becomes true.
addTodo.$isPaused // Store<boolean>onMutate still fires (so optimistic updates work), but mutationFn doesn’t. Resume with:
queryClient.getMutationCache().resumePausedMutations()To opt out (e.g. mutate even when offline), set networkMode: 'always'. To run once but pause retries, use 'offlineFirst'.
Lifecycle: start / unmounted
Symmetric to queries:
start()— subscribe the observerunmounted()— unsubscribe (letsgcTimecollect the mutation entry)
The useMutation hook handles both.