React SME Cookbook
All FAQs

Search Documentation

Search across all documentation pages

swrmutationuseSWRMutationoptimistic

Mutations and Optimistic Updates

Recipe

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>
  );
}

Working Example

"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>
  );
}

Deep Dive

How It Works

  • useSWRMutation is designed for mutations that should not run automatically. It returns a trigger function you call manually.
  • The trigger function receives an arg that gets passed to your mutation function as { arg }.
  • Bound mutate (mutate from useSWR) is scoped to the hook's key. It updates the local cache for that specific key.
  • Global mutate (from 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.

Variations

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,
});

TypeScript Notes

  • useSWRMutation<Data, Error, Key, Arg> takes four generics for full type safety.
  • The mutation function signature is (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 typed

Gotchas

  • useSWRMutation 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.
  • If you forget 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.
  • Calling mutate() with no arguments revalidates the key by re-fetching. Calling mutate(newData) sets data without revalidation.

Alternatives

ApproachProsCons
useSWRMutationPurpose-built, integrates with cacheRequires separate import
Bound mutate + fetchSimpler for basic cache updatesManual error handling
React Query useMutationMore built-in retry and lifecycle hooksDifferent library
Server Actions (Next.js)No client JS, form-nativeNo optimistic UI without extra work

FAQs

What is the difference between useSWRMutation and the mutate function from useSWR?
  • useSWRMutation is for remote mutations (POST, PUT, DELETE) triggered manually via trigger.
  • mutate from useSWR (bound) updates the local cache for a specific key.
  • Global mutate from useSWRConfig can update any cache key from anywhere.
How does the trigger function pass arguments to the mutation function?

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" });
What does optimisticData do and why must it be the full new state?

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.

Gotcha: What happens if I forget rollbackOnError: true with optimistic updates?

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.

How can I revalidate multiple related cache keys after a mutation?

Use the global mutate from useSWRConfig:

const { mutate } = useSWRConfig();
mutate(`/api/posts/${postId}`);
mutate(`/api/posts/${postId}/comments`);
What does the populateCache option do in useSWRMutation?

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,
});
Gotcha: Do useSWRMutation and useSWR share the same cache when using the same key?

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.

What happens to isMutating if I navigate away before the mutation resolves?

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.

How do I type useSWRMutation with full generics in TypeScript?
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 typed
What is the TypeScript signature for a useSWRMutation mutation function?

The 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.

What is the difference between calling mutate() with no arguments vs mutate(newData)?
  • mutate() with no arguments revalidates by re-fetching from the server.
  • mutate(newData) sets the cache data directly without re-fetching.