The framework agnostic core that powers TanStack Query for data fetching and caching
—
Mutation management for data modifications with optimistic updates, rollback capabilities, side effect handling, and automatic query invalidation.
Observer for tracking and executing mutations with automatic state management.
/**
* Observer for managing mutations and tracking their state
* Provides reactive updates for mutation progress, success, and error states
*/
class MutationObserver<
TData = unknown,
TError = Error,
TVariables = void,
TContext = unknown
> {
constructor(client: QueryClient, options: MutationObserverOptions<TData, TError, TVariables, TContext>);
/**
* Execute the mutation with the given variables
* @param variables - Variables to pass to the mutation function
* @param options - Additional options for this specific mutation execution
* @returns Promise resolving to the mutation result
*/
mutate(variables: TVariables, options?: MutateOptions<TData, TError, TVariables, TContext>): Promise<TData>;
/**
* Get the current result snapshot
* @returns Current mutation observer result
*/
getCurrentResult(): MutationObserverResult<TData, TError, TVariables, TContext>;
/**
* Subscribe to mutation state changes
* @param onStoreChange - Callback called when mutation state changes
* @returns Unsubscribe function
*/
subscribe(onStoreChange: (result: MutationObserverResult<TData, TError, TVariables, TContext>) => void): () => void;
/**
* Update mutation observer options
* @param options - New options to merge
*/
setOptions(options: MutationObserverOptions<TData, TError, TVariables, TContext>): void;
/**
* Reset the mutation to its initial state
* Clears data, error, and resets status to idle
*/
reset(): void;
/**
* Destroy the observer and cleanup subscriptions
*/
destroy(): void;
}
interface MutationObserverOptions<
TData = unknown,
TError = Error,
TVariables = void,
TContext = unknown
> {
/**
* Optional mutation key for identification and scoping
*/
mutationKey?: MutationKey;
/**
* The mutation function that performs the actual mutation
* @param variables - Variables passed to the mutation
* @returns Promise resolving to the mutation result
*/
mutationFn?: MutationFunction<TData, TVariables>;
/**
* Function called before the mutation executes
* Useful for optimistic updates
* @param variables - Variables being passed to mutation
* @returns Context value passed to other callbacks
*/
onMutate?: (variables: TVariables) => Promise<TContext> | TContext;
/**
* Function called if the mutation succeeds
* @param data - Data returned by the mutation
* @param variables - Variables that were passed to mutation
* @param context - Context returned from onMutate
*/
onSuccess?: (data: TData, variables: TVariables, context: TContext) => Promise<unknown> | unknown;
/**
* Function called if the mutation fails
* @param error - Error thrown by the mutation
* @param variables - Variables that were passed to mutation
* @param context - Context returned from onMutate
*/
onError?: (error: TError, variables: TVariables, context: TContext) => Promise<unknown> | unknown;
/**
* Function called when the mutation settles (success or error)
* @param data - Data returned by mutation (undefined if error)
* @param error - Error thrown by mutation (null if success)
* @param variables - Variables that were passed to mutation
* @param context - Context returned from onMutate
*/
onSettled?: (data: TData | undefined, error: TError | null, variables: TVariables, context: TContext) => Promise<unknown> | unknown;
/**
* Number of retry attempts for failed mutations
*/
retry?: RetryValue<TError>;
/**
* Delay between retry attempts
*/
retryDelay?: RetryDelayValue<TError>;
/**
* Whether to throw errors or handle them in the result
*/
throwOnError?: ThrowOnError<TData, TError, TVariables, TContext>;
/**
* Additional metadata for the mutation
*/
meta?: MutationMeta;
/**
* Network mode for the mutation
*/
networkMode?: NetworkMode;
/**
* Global mutation ID for scoped mutations
* Only one mutation with the same scope can run at a time
*/
scope?: {
id: string;
};
}
interface MutationObserverResult<
TData = unknown,
TError = Error,
TVariables = void,
TContext = unknown
> {
/** The data returned by the mutation */
data: TData | undefined;
/** The error thrown by the mutation */
error: TError | null;
/** The variables passed to the mutation */
variables: TVariables | undefined;
/** Whether the mutation is currently idle */
isIdle: boolean;
/** Whether the mutation is currently pending */
isPending: boolean;
/** Whether the mutation completed successfully */
isSuccess: boolean;
/** Whether the mutation failed */
isError: boolean;
/** Whether the mutation is paused */
isPaused: boolean;
/** The current status of the mutation */
status: MutationStatus;
/** Number of times the mutation has failed */
failureCount: number;
/** The reason for the most recent failure */
failureReason: TError | null;
/** Function to execute the mutation */
mutate: (variables: TVariables, options?: MutateOptions<TData, TError, TVariables, TContext>) => void;
/** Function to execute the mutation and return a promise */
mutateAsync: (variables: TVariables, options?: MutateOptions<TData, TError, TVariables, TContext>) => Promise<TData>;
/** Function to reset the mutation state */
reset: () => void;
/** Context returned from onMutate */
context: TContext | undefined;
/** Timestamp when the mutation was submitted */
submittedAt: number;
}
interface MutateOptions<
TData = unknown,
TError = Error,
TVariables = void,
TContext = unknown
> {
onSuccess?: (data: TData, variables: TVariables, context: TContext) => Promise<unknown> | unknown;
onError?: (error: TError, variables: TVariables, context: TContext) => Promise<unknown> | unknown;
onSettled?: (data: TData | undefined, error: TError | null, variables: TVariables, context: TContext) => Promise<unknown> | unknown;
throwOnError?: ThrowOnError<TData, TError, TVariables, TContext>;
}Usage Examples:
import { QueryClient, MutationObserver } from "@tanstack/query-core";
const queryClient = new QueryClient();
// Create mutation observer
const mutationObserver = new MutationObserver(queryClient, {
mutationFn: async (userData) => {
const response = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(userData),
});
return response.json();
},
onSuccess: (data, variables) => {
console.log('User created:', data);
// Invalidate users list
queryClient.invalidateQueries({ queryKey: ['users'] });
},
onError: (error, variables) => {
console.error('Failed to create user:', error);
},
});
// Subscribe to mutation state
const unsubscribe = mutationObserver.subscribe((result) => {
console.log('Status:', result.status);
console.log('Is pending:', result.isPending);
console.log('Data:', result.data);
console.log('Error:', result.error);
if (result.isSuccess) {
console.log('Mutation succeeded with data:', result.data);
}
if (result.isError) {
console.error('Mutation failed with error:', result.error);
}
});
// Execute mutation
try {
const newUser = await mutationObserver.mutate({
name: 'John Doe',
email: 'john@example.com',
});
console.log('Created user:', newUser);
} catch (error) {
console.error('Mutation failed:', error);
}
// Reset mutation state
mutationObserver.reset();
// Cleanup
unsubscribe();
mutationObserver.destroy();Implementing optimistic updates with proper rollback handling.
// Optimistic updates with rollback
const updateUserMutation = new MutationObserver(queryClient, {
mutationFn: async ({ id, updates }) => {
const response = await fetch(`/api/users/${id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updates),
});
if (!response.ok) throw new Error('Update failed');
return response.json();
},
// Optimistic update
onMutate: async ({ id, updates }) => {
// Cancel outgoing refetches so they don't overwrite optimistic update
await queryClient.cancelQueries({ queryKey: ['user', id] });
// Snapshot the previous value
const previousUser = queryClient.getQueryData(['user', id]);
// Optimistically update the cache
queryClient.setQueryData(['user', id], (old) => ({
...old,
...updates,
}));
// Return context with previous value for rollback
return { previousUser };
},
// Rollback on error
onError: (error, { id }, context) => {
// Restore previous value on error
if (context?.previousUser) {
queryClient.setQueryData(['user', id], context.previousUser);
}
},
// Always refetch after success or error
onSettled: (data, error, { id }) => {
queryClient.invalidateQueries({ queryKey: ['user', id] });
},
});Direct access to mutation cache for advanced scenarios.
/**
* Resume all paused mutations
* Useful after network reconnection
* @returns Promise that resolves when all mutations are resumed
*/
resumePausedMutations(): Promise<unknown>;
/**
* Get the mutation cache instance
* @returns The mutation cache
*/
getMutationCache(): MutationCache;
/**
* Check how many mutations are currently running
* @param filters - Optional filters to narrow the count
* @returns Number of pending mutations
*/
isMutating(filters?: MutationFilters): number;Usage Examples:
// Resume paused mutations after reconnection
await queryClient.resumePausedMutations();
// Check if any mutations are running
const mutationCount = queryClient.isMutating();
if (mutationCount > 0) {
console.log(`${mutationCount} mutations currently running`);
}
// Check specific mutations
const userMutationCount = queryClient.isMutating({
mutationKey: ['user'],
});
// Access mutation cache directly
const mutationCache = queryClient.getMutationCache();
const allMutations = mutationCache.getAll();
console.log('All mutations:', allMutations);Managing concurrent mutations with scoping to prevent conflicts.
interface MutationObserverOptions<T> {
/**
* Scope configuration for limiting concurrent mutations
* Only one mutation with the same scope ID can run at once
*/
scope?: {
id: string;
};
}Usage Examples:
// Scoped mutations - only one per user at a time
const createScopedUserMutation = (userId) => new MutationObserver(queryClient, {
mutationFn: async (updates) => {
const response = await fetch(`/api/users/${userId}`, {
method: 'PATCH',
body: JSON.stringify(updates),
});
return response.json();
},
scope: {
id: `user-${userId}`, // Only one mutation per user
},
});
// Global scope - only one mutation of this type at a time
const globalMutation = new MutationObserver(queryClient, {
mutationFn: async (data) => {
// Critical operation that should not run concurrently
const response = await fetch('/api/critical-operation', {
method: 'POST',
body: JSON.stringify(data),
});
return response.json();
},
scope: {
id: 'critical-operation',
},
});Common patterns for handling side effects after mutations.
// Complex side effects with multiple invalidations
const complexMutation = new MutationObserver(queryClient, {
mutationFn: async (data) => {
const response = await fetch('/api/complex-operation', {
method: 'POST',
body: JSON.stringify(data),
});
return response.json();
},
onSuccess: async (data, variables) => {
// Invalidate multiple related queries
await Promise.all([
queryClient.invalidateQueries({ queryKey: ['users'] }),
queryClient.invalidateQueries({ queryKey: ['posts', data.userId] }),
queryClient.invalidateQueries({ queryKey: ['notifications'] }),
]);
// Update specific cached data
queryClient.setQueryData(['user', data.userId], (old) => ({
...old,
lastActivity: new Date().toISOString(),
}));
// Prefetch related data
await queryClient.prefetchQuery({
queryKey: ['user-stats', data.userId],
queryFn: () => fetch(`/api/users/${data.userId}/stats`).then(r => r.json()),
});
},
onError: (error, variables) => {
// Handle specific error types
if (error.status === 409) {
// Conflict - refresh conflicting data
queryClient.invalidateQueries({ queryKey: ['conflicts'] });
} else if (error.status >= 500) {
// Server error - maybe retry later
console.error('Server error, consider retrying:', error);
}
},
});type MutationKey = ReadonlyArray<unknown>;
type MutationFunction<TData = unknown, TVariables = void> = (variables: TVariables) => Promise<TData>;
type MutationStatus = 'idle' | 'pending' | 'success' | 'error';
type RetryValue<TError> = boolean | number | ((failureCount: number, error: TError) => boolean);
type RetryDelayValue<TError> = number | ((failureCount: number, error: TError, mutation: Mutation) => number);
type ThrowOnError<TData, TError, TVariables, TContext> =
| boolean
| ((error: TError, variables: TVariables, context: TContext | undefined) => boolean);
type NetworkMode = 'online' | 'always' | 'offlineFirst';
interface MutationMeta extends Record<string, unknown> {}
interface MutationFilters {
mutationKey?: MutationKey;
exact?: boolean;
predicate?: (mutation: Mutation) => boolean;
status?: MutationStatus;
}Install with Tessl CLI
npx tessl i tessl/npm-tanstack--query-core