Hooks for managing, caching and syncing asynchronous and remote data in Vue
—
Composables for creating, updating, and deleting data with optimistic updates, automatic query invalidation, and reactive state management.
Main composable for data mutations with automatic error handling and query invalidation.
/**
* Main composable for data mutations with optimistic updates
* @param options - Mutation configuration options with Vue reactivity support
* @param queryClient - Optional query client instance
* @returns Reactive mutation state and execution functions
*/
function useMutation<TData, TError, TVariables, TContext>(
options: UseMutationOptions<TData, TError, TVariables, TContext>,
queryClient?: QueryClient
): UseMutationReturnType<TData, TError, TVariables, TContext>;
interface UseMutationOptions<TData, TError, TVariables, TContext> {
mutationFn?: MaybeRefOrGetter<
(variables: TVariables) => Promise<TData> | TData
>;
mutationKey?: MaybeRefOrGetter<MutationKey>;
onMutate?: MaybeRefOrGetter<
(variables: TVariables) => Promise<TContext | void> | TContext | void
>;
onSuccess?: MaybeRefOrGetter<
(data: TData, variables: TVariables, context: TContext | undefined) => Promise<unknown> | unknown
>;
onError?: MaybeRefOrGetter<
(error: TError, variables: TVariables, context: TContext | undefined) => Promise<unknown> | unknown
>;
onSettled?: MaybeRefOrGetter<
(data: TData | undefined, error: TError | null, variables: TVariables, context: TContext | undefined) => Promise<unknown> | unknown
>;
retry?: MaybeRefOrGetter<boolean | number | ((failureCount: number, error: TError) => boolean)>;
retryDelay?: MaybeRefOrGetter<number | ((retryAttempt: number, error: TError) => number)>;
throwOnError?: MaybeRefOrGetter<boolean | ((error: TError) => boolean)>;
meta?: MaybeRefOrGetter<MutationMeta>;
shallow?: boolean;
}
interface UseMutationReturnType<TData, TError, TVariables, TContext> {
data: Ref<TData | undefined>;
error: Ref<TError | null>;
failureCount: Ref<number>;
failureReason: Ref<TError | null>;
isError: Ref<boolean>;
isIdle: Ref<boolean>;
isPending: Ref<boolean>;
isPaused: Ref<boolean>;
isSuccess: Ref<boolean>;
mutate: (variables: TVariables, options?: MutateOptions<TData, TError, TVariables, TContext>) => void;
mutateAsync: (variables: TVariables, options?: MutateOptions<TData, TError, TVariables, TContext>) => Promise<TData>;
reset: () => void;
status: Ref<MutationStatus>;
submittedAt: Ref<number>;
variables: Ref<TVariables | undefined>;
}
interface MutateOptions<TData, TError, TVariables, TContext> {
onSuccess?: (data: TData, variables: TVariables, context: TContext | undefined) => Promise<unknown> | unknown;
onError?: (error: TError, variables: TVariables, context: TContext | undefined) => Promise<unknown> | unknown;
onSettled?: (data: TData | undefined, error: TError | null, variables: TVariables, context: TContext | undefined) => Promise<unknown> | unknown;
}Usage Examples:
import { useMutation, useQueryClient } from '@tanstack/vue-query';
// Basic mutation
const { mutate, isPending, error } = useMutation({
mutationFn: (newPost) =>
fetch('/api/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newPost)
}).then(res => res.json()),
onSuccess: () => {
console.log('Post created successfully!');
},
onError: (error) => {
console.error('Failed to create post:', error);
}
});
// Trigger mutation
const createPost = () => {
mutate({
title: 'New Post',
content: 'This is the content'
});
};
// Mutation with query invalidation
const queryClient = useQueryClient();
const { mutate: updateUser } = useMutation({
mutationFn: ({ id, data }) =>
fetch(`/api/users/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
}).then(res => res.json()),
onSuccess: (data, variables) => {
// Invalidate and refetch user queries
queryClient.invalidateQueries({ queryKey: ['users'] });
queryClient.invalidateQueries({ queryKey: ['user', variables.id] });
}
});
// Optimistic updates
const { mutate: toggleTodo } = useMutation({
mutationFn: ({ id, completed }) =>
fetch(`/api/todos/${id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ completed })
}).then(res => res.json()),
onMutate: async ({ id, completed }) => {
// 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 === id ? { ...todo, completed } : todo
)
);
// Return context for rollback
return { previousTodos };
},
onError: (err, variables, context) => {
// Rollback on error
if (context?.previousTodos) {
queryClient.setQueryData(['todos'], context.previousTodos);
}
},
onSettled: () => {
// Always refetch after error or success
queryClient.invalidateQueries({ queryKey: ['todos'] });
}
});
// Async mutation with error handling
const { mutateAsync: deletePost } = useMutation({
mutationFn: (postId) =>
fetch(`/api/posts/${postId}`, { method: 'DELETE' })
.then(res => {
if (!res.ok) throw new Error('Failed to delete');
return res.json();
})
});
// Using async mutation
const handleDelete = async (postId) => {
try {
await deletePost(postId);
router.push('/posts');
} catch (error) {
alert('Failed to delete post');
}
};
// Mutation with retry logic
const { mutate: uploadFile } = useMutation({
mutationFn: (file) => {
const formData = new FormData();
formData.append('file', file);
return fetch('/api/upload', {
method: 'POST',
body: formData
}).then(res => res.json());
},
retry: 3,
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
onError: (error, variables, context) => {
console.error(`Upload failed after retries:`, error);
}
});
// Multiple mutations with shared state
const { mutate: saveDraft, isPending: isSaving } = useMutation({
mutationFn: (draft) => fetch('/api/drafts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(draft)
})
});
const { mutate: publishPost, isPending: isPublishing } = useMutation({
mutationFn: (post) => fetch('/api/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(post)
})
});
const isBusy = computed(() => isSaving.value || isPublishing.value);// Mutation key type
type MutationKey = ReadonlyArray<unknown>;
// Mutation status type
type MutationStatus = 'idle' | 'pending' | 'success' | 'error';
// Mutation function context
interface MutationObserverBaseResult<TData, TError, TVariables, TContext> {
context: TContext | undefined;
data: TData | undefined;
error: TError | null;
failureCount: number;
failureReason: TError | null;
isPaused: boolean;
status: MutationStatus;
submittedAt: number;
variables: TVariables | undefined;
}
// Mutation metadata
interface MutationMeta extends Record<string, unknown> {
[key: string]: unknown;
}
// Base mutation options
interface MutationObserverOptions<TData = unknown, TError = DefaultError, TVariables = void, TContext = unknown> {
mutationFn?: (variables: TVariables) => Promise<TData> | TData;
mutationKey?: MutationKey;
onMutate?: (variables: TVariables) => Promise<TContext | void> | TContext | void;
onSuccess?: (data: TData, variables: TVariables, context: TContext | undefined) => Promise<unknown> | unknown;
onError?: (error: TError, variables: TVariables, context: TContext | undefined) => Promise<unknown> | unknown;
onSettled?: (data: TData | undefined, error: TError | null, variables: TVariables, context: TContext | undefined) => Promise<unknown> | unknown;
retry?: boolean | number | ((failureCount: number, error: TError) => boolean);
retryDelay?: number | ((retryAttempt: number, error: TError) => number);
throwOnError?: boolean | ((error: TError) => boolean);
meta?: MutationMeta;
}
// Vue-specific mutation options type
type UseMutationOptionsBase<TData, TError, TVariables, TContext> =
MutationObserverOptions<TData, TError, TVariables, TContext> & ShallowOption;
// Default error type
type DefaultError = Error;Install with Tessl CLI
npx tessl i tessl/npm-tanstack--vue-query