Hooks for managing, caching and syncing asynchronous and remote data in React
—
Data mutation operations with optimistic updates, automatic error handling, and cache invalidation patterns. The useMutation hook handles side effects like creating, updating, or deleting data.
The main hook for performing data mutations with loading states and error handling.
/**
* Create a mutation for performing side effects
* @param options - Mutation configuration options
* @returns Mutation result with mutate functions and states
*/
function useMutation<TData = unknown, TError = unknown, TVariables = void, TContext = unknown>(
options: UseMutationOptions<TData, TError, TVariables, TContext>
): UseMutationResult<TData, TError, TVariables, TContext>;
/**
* Create a mutation with separate mutationFn parameter
* @param mutationFn - Function that performs the mutation
* @param options - Additional mutation configuration
* @returns Mutation result with mutate functions and states
*/
function useMutation<TData = unknown, TError = unknown, TVariables = void, TContext = unknown>(
mutationFn: MutationFunction<TData, TVariables>,
options?: Omit<UseMutationOptions<TData, TError, TVariables, TContext>, 'mutationFn'>
): UseMutationResult<TData, TError, TVariables, TContext>;
/**
* Create a mutation with mutation key for tracking and deduplication
* @param mutationKey - Unique identifier for the mutation
* @param options - Mutation configuration
* @returns Mutation result with mutate functions and states
*/
function useMutation<TData = unknown, TError = unknown, TVariables = void, TContext = unknown>(
mutationKey: MutationKey,
mutationFn: MutationFunction<TData, TVariables>,
options?: Omit<UseMutationOptions<TData, TError, TVariables, TContext>, 'mutationKey' | 'mutationFn'>
): UseMutationResult<TData, TError, TVariables, TContext>;Usage Examples:
import { useMutation, useQueryClient } from "react-query";
// Basic mutation
const createPost = useMutation({
mutationFn: (newPost) => fetch('/api/posts', {
method: 'POST',
body: JSON.stringify(newPost)
}).then(res => res.json()),
onSuccess: () => {
alert('Post created!');
}
});
// Mutation with cache invalidation
const updateUser = useMutation({
mutationFn: ({ id, data }) => fetch(`/api/users/${id}`, {
method: 'PUT',
body: JSON.stringify(data)
}),
onSuccess: (data, variables) => {
queryClient.invalidateQueries({ queryKey: ['user', variables.id] });
}
});
// Optimistic mutation
const toggleTodo = useMutation({
mutationFn: ({ id, completed }) =>
fetch(`/api/todos/${id}`, {
method: 'PATCH',
body: JSON.stringify({ completed })
}),
onMutate: async (variables) => {
// Cancel outgoing refetches
await queryClient.cancelQueries({ queryKey: ['todos'] });
// Snapshot previous value
const previousTodos = queryClient.getQueryData(['todos']);
// Optimistically update
queryClient.setQueryData(['todos'], (old) =>
old.map(todo =>
todo.id === variables.id
? { ...todo, completed: variables.completed }
: todo
)
);
return { previousTodos };
},
onError: (err, variables, context) => {
// Rollback on error
queryClient.setQueryData(['todos'], context.previousTodos);
},
onSettled: () => {
// Always refetch after error or success
queryClient.invalidateQueries({ queryKey: ['todos'] });
}
});
// Using the mutations
const handleCreatePost = () => {
createPost.mutate({ title: 'New Post', content: 'Content' });
};
const handleUpdateAsync = async () => {
try {
const result = await updateUser.mutateAsync({
id: 1,
data: { name: 'Updated Name' }
});
console.log('Update successful:', result);
} catch (error) {
console.error('Update failed:', error);
}
};The return value from useMutation containing mutation functions and states.
interface UseMutationResult<TData = unknown, TError = unknown, TVariables = unknown, TContext = unknown> {
/** Function to trigger the mutation (fire-and-forget) */
mutate: UseMutateFunction<TData, TError, TVariables, TContext>;
/** Async function to trigger mutation and return promise */
mutateAsync: UseMutateAsyncFunction<TData, TError, TVariables, TContext>;
/** Data returned from successful mutation */
data: TData | undefined;
/** Error object if mutation failed */
error: TError | null;
/** True if mutation is in error state */
isError: boolean;
/** True if mutation is currently running */
isPending: boolean;
/** True if mutation succeeded */
isSuccess: boolean;
/** True if mutation has been triggered at least once */
isIdle: boolean;
/** Current status of the mutation */
status: 'idle' | 'pending' | 'error' | 'success';
/** Variables passed to the last mutation call */
variables: TVariables | undefined;
/** Context returned from onMutate */
context: TContext | undefined;
/** Number of times mutation has failed */
failureCount: number;
/** Reason mutation is paused */
failureReason: TError | null;
/** True if mutation is paused */
isPaused: boolean;
/** Function to reset mutation state */
reset: () => void;
}
type UseMutateFunction<TData = unknown, TError = unknown, TVariables = void, TContext = unknown> = (
variables: TVariables,
options?: MutateOptions<TData, TError, TVariables, TContext>
) => void;
type UseMutateAsyncFunction<TData = unknown, TError = unknown, TVariables = void, TContext = unknown> = (
variables: TVariables,
options?: MutateOptions<TData, TError, TVariables, TContext>
) => Promise<TData>;Configuration options for useMutation hook.
interface UseMutationOptions<TData = unknown, TError = unknown, TVariables = void, TContext = unknown> {
/** Unique identifier for the mutation */
mutationKey?: MutationKey;
/** Function that performs the mutation */
mutationFn?: MutationFunction<TData, TVariables>;
/** Retry configuration */
retry?: boolean | number | ((failureCount: number, error: TError) => boolean);
/** Delay between retries */
retryDelay?: number | ((retryAttempt: number, error: TError) => number);
/** Callback before mutation starts (for optimistic updates) */
onMutate?: (variables: TVariables) => Promise<TContext> | TContext | void;
/** Callback on successful mutation */
onSuccess?: (data: TData, variables: TVariables, context: TContext | undefined) => Promise<unknown> | unknown;
/** Callback on mutation error */
onError?: (error: TError, variables: TVariables, context: TContext | undefined) => Promise<unknown> | unknown;
/** Callback after mutation settles (success or error) */
onSettled?: (data: TData | undefined, error: TError | null, variables: TVariables, context: TContext | undefined) => Promise<unknown> | unknown;
/** React context for QueryClient */
context?: React.Context<QueryClient | undefined>;
/** Whether to throw errors to error boundaries */
useErrorBoundary?: boolean | ((error: TError, variables: TVariables, context: TContext | undefined) => boolean);
/** Additional metadata */
meta?: MutationMeta;
/** Network mode configuration */
networkMode?: 'online' | 'always' | 'offlineFirst';
}
interface MutateOptions<TData = unknown, TError = unknown, TVariables = void, TContext = unknown> {
/** Override onSuccess for this call */
onSuccess?: (data: TData, variables: TVariables, context: TContext | undefined) => Promise<unknown> | unknown;
/** Override onError for this call */
onError?: (error: TError, variables: TVariables, context: TContext | undefined) => Promise<unknown> | unknown;
/** Override onSettled for this call */
onSettled?: (data: TData | undefined, error: TError | null, variables: TVariables, context: TContext | undefined) => Promise<unknown> | unknown;
}The function that actually performs the mutation.
type MutationFunction<TData = unknown, TVariables = void> = (
variables: TVariables
) => Promise<TData>;
type MutationKey = readonly unknown[];
type MutationMeta = Record<string, unknown>;Different strategies for updating cached data after mutations:
const queryClient = useQueryClient();
// 1. Invalidation (refetch)
const mutation = useMutation({
mutationFn: updateUser,
onSuccess: (data, variables) => {
// Invalidate and refetch
queryClient.invalidateQueries({ queryKey: ['user', variables.id] });
queryClient.invalidateQueries({ queryKey: ['users'] });
}
});
// 2. Direct cache update
const mutation = useMutation({
mutationFn: updateUser,
onSuccess: (updatedUser, variables) => {
// Update specific user query
queryClient.setQueryData(['user', variables.id], updatedUser);
// Update users list
queryClient.setQueryData(['users'], (oldUsers) =>
oldUsers.map(user =>
user.id === variables.id ? updatedUser : user
)
);
}
});
// 3. Add to cache (for create operations)
const createMutation = useMutation({
mutationFn: createUser,
onSuccess: (newUser) => {
// Add to users list
queryClient.setQueryData(['users'], (oldUsers) => [
...oldUsers,
newUser
]);
// Set individual user query
queryClient.setQueryData(['user', newUser.id], newUser);
}
});Monitor all mutations using useIsMutating:
import { useIsMutating } from "react-query";
function GlobalLoadingIndicator() {
const isMutating = useIsMutating();
if (isMutating) {
return <div>Saving changes...</div>;
}
return null;
}
// Filter specific mutations
function UserUpdateIndicator({ userId }: { userId: string }) {
const isUpdatingUser = useIsMutating({
mutationKey: ['updateUser', userId]
});
return isUpdatingUser ? <div>Updating user...</div> : null;
}const mutation = useMutation({
mutationFn: updateData,
retry: (failureCount, error) => {
// Don't retry client errors (4xx)
if (error.status >= 400 && error.status < 500) {
return false;
}
// Retry server errors up to 3 times
return failureCount < 3;
},
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
onError: (error, variables, context) => {
// Handle error
toast.error(`Failed to update: ${error.message}`);
// Log for analytics
analytics.track('mutation_error', {
mutation: 'updateData',
error: error.message,
variables
});
}
});
// Manual retry
if (mutation.isError) {
return (
<div>
<p>Update failed: {mutation.error.message}</p>
<button onClick={() => mutation.reset()}>
Dismiss
</button>
<button onClick={() => mutation.mutate(lastVariables)}>
Retry
</button>
</div>
);
}Chain mutations together:
function useSequentialMutations() {
const queryClient = useQueryClient();
const createUser = useMutation({
mutationFn: (userData) => api.createUser(userData)
});
const assignRole = useMutation({
mutationFn: ({ userId, roleId }) => api.assignRole(userId, roleId)
});
const sendWelcomeEmail = useMutation({
mutationFn: (userId) => api.sendWelcomeEmail(userId)
});
const createUserWithRole = async (userData, roleId) => {
try {
const user = await createUser.mutateAsync(userData);
await assignRole.mutateAsync({ userId: user.id, roleId });
await sendWelcomeEmail.mutateAsync(user.id);
// Refresh users list
queryClient.invalidateQueries({ queryKey: ['users'] });
return user;
} catch (error) {
// Handle any step failure
console.error('User creation process failed:', error);
throw error;
}
};
return {
createUserWithRole,
isLoading: createUser.isPending || assignRole.isPending || sendWelcomeEmail.isPending
};
}Install with Tessl CLI
npx tessl i tessl/npm-react-query