Mutations
Create, update, delete operations and cache invalidation.
Creating Mutations
Use the mutation() method for write operations:
class ProjectsClient extends QueryClient<"projects"> {
createProject() {
return this.mutation<
"create", // Mutation key
Project, // Return type
ApiError, // Error type
{ name: string; slug: string } // Variables type
>({
key: ["create"],
fn: async (data) => {
const response = await fetch("/api/projects", {
method: "POST",
body: JSON.stringify(data),
});
return response.json();
},
onSuccess: () => {
this.refresh("list"); // Invalidate list queries
},
});
}
}Using Mutations
import { useMutation } from "@tanstack/react-query";
function CreateProjectForm() {
const projectsClient = useProjectsClient();
const { mutate, isPending, error } = useMutation(projectsClient.createProject());
const handleSubmit = (formData) => {
mutate(formData, {
onSuccess: (newProject) => {
// Navigate to new project
router.push(`/projects/${newProject.id}`);
},
});
};
return (
<form onSubmit={handleSubmit}>
<input name="name" placeholder="Project name" />
<input name="slug" placeholder="project-slug" />
<button type="submit" disabled={isPending}>
{isPending ? "Creating..." : "Create Project"}
</button>
{error && <p className="error">{error.message}</p>}
</form>
);
}Cache Invalidation
Invalidate related queries after mutations to keep data fresh:
class ProjectsClient extends QueryClient<"projects"> {
updateProject(projectId: string) {
return this.mutation({
key: ["update", projectId],
fn: async (data: UpdateProjectInput) => {
const response = await fetch(`/api/projects/${projectId}`, {
method: "PATCH",
body: JSON.stringify(data),
});
return response.json();
},
onSuccess: () => {
// Invalidate both list and detail queries
this.refresh("list");
this.refresh("byId", projectId);
},
});
}
deleteProject(projectId: string) {
return this.mutation({
key: ["delete", projectId],
fn: async () => {
await fetch(`/api/projects/${projectId}`, { method: "DELETE" });
},
onSuccess: () => {
// Invalidate all project queries
this.refresh();
},
});
}
}Optimistic Updates
Update the cache before the mutation completes:
updateProject(projectId: string) {
return this.mutation({
key: ["update", projectId],
fn: updateProjectApi,
onMutate: async (newData) => {
// Cancel outgoing refetches
await queryClient.cancelQueries({ queryKey: ["@teardown", "projects"] });
// Snapshot previous value
const previous = this.getProject(projectId).get();
// Optimistically update
this.getProject(projectId).set({ ...previous, ...newData });
return { previous };
},
onError: (_err, _vars, context) => {
// Rollback on error
if (context?.previous) {
this.getProject(projectId).set(context.previous);
}
},
onSettled: () => {
// Refetch after mutation
this.refresh("byId", projectId);
},
});
}Error Handling
Handle errors consistently in mutations:
deleteProject(projectId: string) {
return this.mutation({
key: ["delete", projectId],
fn: async () => {
const response = await fetch(`/api/projects/${projectId}`, {
method: "DELETE",
});
if (!response.ok) {
const error = await response.json();
throw error;
}
},
onError: (error) => {
if (error.status === 404) {
// Handle not found
toast.error("Project not found");
} else if (error.status === 403) {
// Handle forbidden
toast.error("You don't have permission to delete this project");
} else {
toast.error("Failed to delete project");
}
},
});
}Mutation Callbacks
All TanStack Query mutation callbacks are supported:
this.mutation({
key: ["create"],
fn: createProjectFn,
// Before mutation starts
onMutate: async (variables) => {
// Return context for rollback
return { previousData };
},
// On success
onSuccess: (data, variables, context) => {
// Update cache, show toast, navigate
},
// On error
onError: (error, variables, context) => {
// Rollback, show error
},
// Always runs (success or error)
onSettled: (data, error, variables, context) => {
// Cleanup, refetch
},
});Mutation Result
interface CreateMutationResult<Resource, MKey, TData, TError, TVariables, TContext> {
mutationKey: MutationKey<Resource, MKey>;
mutationFn: MutationFunction<TData, TVariables>;
// ... other MutationOptions
}Cross-Resource Invalidation
Invalidate queries across multiple resources:
class ProjectsClient extends QueryClient<"projects"> {
constructor(
tanstackClient: TanstackQueryClient,
private readonly orgsClient: OrgsClient
) {
super(tanstackClient, "projects");
}
deleteProject(projectId: string) {
return this.mutation({
key: ["delete", projectId],
fn: deleteProjectFn,
onSuccess: () => {
// Invalidate project queries
this.refresh();
// Also invalidate org stats (cross-resource)
this.orgsClient.refresh("stats");
},
});
}
}