Search across all documentation pages
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Use useSWRMutation for remote mutations (POST, PUT, DELETE) and mutate for local cache updates. Combine both for optimistic UI patterns that update instantly and reconcile with the server.
"use client";
import useSWRMutation from "swr/mutation";
async function createPost(url: string, { arg }: { arg: { title: string; body: string } }) {
const res = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(arg),
});
return res.json();
}
function NewPostForm() {
const { trigger, isMutating } = useSWRMutation("/api/posts", createPost);
const handleSubmit = async (formData: FormData) => {
await trigger({
title: formData.get("title") as string,
body: formData.get("body") as string,
});
};
return (
<form action={handleSubmit}>
<input name="title" required />
<textarea name="body" required />
<button disabled={isMutating}>
{isMutating ? "Creating..." : "Create Post"}
</button>
</form>
);
}"use client";
import useSWR, { useSWRConfig } from "swr";
import useSWRMutation from "swr/mutation";
interface Todo {
id: number;
text: string;
done: boolean;
}
const fetcher = (url: string): Promise<Todo[]> => fetch(url).then((r) => r.json());
async function toggleTodo(url: string, { arg }: { arg: { id: number; done: boolean } }) {
return fetch(`${url}/${arg.id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ done: arg.done }),
}).then((r) => r.json());
}
export default function TodoList() {
const { data: todos, mutate } = useSWR<Todo[]>("/api/todos", fetcher);
const { trigger } = useSWRMutation("/api/todos", toggleTodo);
const handleToggle = async (todo: Todo) => {
const newDone = !todo.done;
// Optimistic update
await mutate(
async (current) => {
await trigger({ id: todo.id, done: newDone });
return current?.map((t) => (t.id === todo.id ? { ...t, done: newDone } : t));
},
{
optimisticData: todos?.map((t) =>
t.id === todo.id ? { ...t, done: newDone } : t
),
rollbackOnError: true,
revalidate: false,
}
);
};
return (
<ul>
{todos?.map((todo) => (
<li key={todo.id} onClick={() => handleToggle(todo)}>
<span style={{ textDecoration: todo.done ? "line-through" : "none" }}>
{todo.text}
</span>
</li>
))}
</ul>
);
}useSWRMutation is designed for mutations that should not run automatically. It returns a trigger function you call manually.trigger function receives an arg that gets passed to your mutation function as { arg }.mutate from useSWR) is scoped to the hook's key. It updates the local cache for that specific key.useSWRConfig) can update any cache key from anywhere in your app.optimisticData immediately updates the cache before the async mutation resolves.rollbackOnError: true reverts the optimistic update if the mutation throws.revalidate: false skips re-fetching after mutation when you trust the local update.Global mutate to invalidate related keys:
import { useSWRConfig } from "swr";
function AddComment({ postId }: { postId: string }) {
const { mutate } = useSWRConfig();
const handleAdd = async (text: string) => {
await fetch(`/api/posts/${postId}/comments`, {
method: "POST",
body: JSON.stringify({ text }),
});
// Revalidate multiple keys
mutate(`/api/posts/${postId}`);
mutate(`/api/posts/${postId}/comments`);
};
}Mutation with returned data:
const { trigger } = useSWRMutation("/api/posts", createPost, {
onSuccess(data) {
// data is the return value from createPost
console.log("Created:", data.id);
},
});Populate cache after mutation:
const { trigger } = useSWRMutation("/api/posts", createPost, {
populateCache: (newPost, currentPosts) => [...(currentPosts ?? []), newPost],
revalidate: false,
});useSWRMutation<Data, Error, Key, Arg> takes four generics for full type safety.(key: Key, options: { arg: Arg }) => Promise<Data>.const { trigger } = useSWRMutation<Todo, Error, string, { text: string }>(
"/api/todos",
async (url, { arg }) => {
const res = await fetch(url, {
method: "POST",
body: JSON.stringify(arg),
});
return res.json();
}
);
// trigger({ text: "Buy milk" }) - fully typeduseSWRMutation and useSWR use the same cache. If both use the same key, they share data. This is usually what you want, but be aware that useSWRMutation's trigger can overwrite useSWR's cached data.optimisticData must be the full new state, not a partial update. If you have a list, you need to return the entire updated list.rollbackOnError: true, a failed mutation leaves stale optimistic data in the cache.isMutating stays true until the trigger promise resolves. If you navigate away before it resolves, there is no cleanup unless the component unmounts.mutate() with no arguments revalidates the key by re-fetching. Calling mutate(newData) sets data without revalidation.| Approach | Pros | Cons |
|---|---|---|
| useSWRMutation | Purpose-built, integrates with cache | Requires separate import |
| Bound mutate + fetch | Simpler for basic cache updates | Manual error handling |
| React Query useMutation | More built-in retry and lifecycle hooks | Different library |
| Server Actions (Next.js) | No client JS, form-native | No optimistic UI without extra work |
useSWRMutation is for remote mutations (POST, PUT, DELETE) triggered manually via trigger.mutate from useSWR (bound) updates the local cache for a specific key.mutate from useSWRConfig can update any cache key from anywhere.The argument passed to trigger(arg) is available in the mutation function as { arg }:
async function createPost(url: string, { arg }: { arg: { title: string } }) {
return fetch(url, {
method: "POST",
body: JSON.stringify(arg),
}).then((r) => r.json());
}
const { trigger } = useSWRMutation("/api/posts", createPost);
await trigger({ title: "Hello" });optimisticData immediately updates the cache before the async mutation resolves. It must be the complete new state (e.g., the entire updated list), not a partial update, because SWR replaces the entire cache entry with it.
If the mutation fails, the stale optimistic data remains in the cache permanently. The UI will show data that does not match the server state. Always set rollbackOnError: true when using optimisticData.
Use the global mutate from useSWRConfig:
const { mutate } = useSWRConfig();
mutate(`/api/posts/${postId}`);
mutate(`/api/posts/${postId}/comments`);It lets you update the cache directly with the mutation's return value without a revalidation fetch:
const { trigger } = useSWRMutation("/api/posts", createPost, {
populateCache: (newPost, currentPosts) => [...(currentPosts ?? []), newPost],
revalidate: false,
});Yes. If both use the same key, they share cached data. The trigger from useSWRMutation can overwrite data that useSWR is displaying. This is usually intended but can cause surprises if unexpected.
isMutating stays true until the trigger promise resolves. If the component unmounts before resolution, there is no cleanup. Be cautious with navigation during in-flight mutations.
const { trigger } = useSWRMutation<Todo, Error, string, { text: string }>(
"/api/todos",
async (url, { arg }) => {
const res = await fetch(url, {
method: "POST",
body: JSON.stringify(arg),
});
return res.json();
}
);
// trigger({ text: "Buy milk" }) is fully typedThe signature is (key: Key, options: { arg: Arg }) => Promise<Data>. The four generics on useSWRMutation<Data, Error, Key, Arg> control the types for return data, error, key, and the argument passed to trigger.
mutate() with no arguments revalidates by re-fetching from the server.mutate(newData) sets the cache data directly without re-fetching.Reviewed by Chris St. John·Last updated Jul 10, 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥