AiTechWorlds
AiTechWorlds
University learning companion
React & Next.js Complete Course · Optimistic Updates & Mutations
A feature looks correct on a developer’s large monitor but fails on a slow connection and a small screen. Use this topic to inspect structure, state, loading/error behaviour, accessibility and the user journey—not just the happy path.
Use this course-level map to place “Optimistic Updates & Mutations” in the larger system. It complements—not replaces—a topic-specific worked diagram.
An optimistic update changes the UI immediately, before the server confirms the action, then rolls back only if the server rejects it.
It works like a cashier handing you your receipt the instant you tap your card, trusting the bank to approve the charge a moment later, rather than making you stand and wait for confirmation before you can leave. When a user clicks "Like," the like count jumps instantly; the server request runs in the background, and the UI rolls back only if that request fails. The result feels instant even on slow connections.
User clicks "Like"
→ Immediately update local state (optimistic)
→ Send request to server in background
→ If server succeeds: done (keep the optimistic state)
→ If server fails: roll back to previous state + show errorReact 19 ships useOptimistic the cleanest way to implement this pattern:
"use client";
import { useOptimistic, useTransition } from "react";
interface Post {
id: string;
title: string;
likes: number;
liked: boolean;
}
function PostCard({ post }: { post: Post }) {
const [isPending, startTransition] = useTransition();
const [optimisticPost, addOptimisticUpdate] = useOptimistic(
post,
(currentPost, liked: boolean) => ({
...currentPost,
liked,
likes: liked ? currentPost.likes + 1 : currentPost.likes - 1,
})
);
function handleLike() {
const newLiked = !optimisticPost.liked;
startTransition(async () => {
addOptimisticUpdate(newLiked); // Update UI immediately
await fetch(`/api/posts/${post.id}/like`, {
method: newLiked ? "POST" : "DELETE",
});
// Server action completes — React merges real state
});
}
return (
<div>
<h3>{post.title}</h3>
<button
onClick={handleLike}
className={optimisticPost.liked ? "text-red-500" : "text-gray-400"}
disabled={isPending}
>
♥ {optimisticPost.likes}
</button>
</div>
);
}"use client";
import { useMutation, useQueryClient } from "@tanstack/react-query";
interface Todo {
id: string;
text: string;
completed: boolean;
}
function TodoItem({ todo }: { todo: Todo }) {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (completed: boolean) =>
fetch(`/api/todos/${todo.id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ completed }),
}).then(r => r.json()),
// Before the mutation fires: optimistically update the cache
onMutate: async (completed) => {
// Cancel any outgoing refetches (prevent overwrites)
await queryClient.cancelQueries({ queryKey: ["todos"] });
// Snapshot the previous value (for rollback)
const previous = queryClient.getQueryData<Todo[]>(["todos"]);
// Optimistically update the cache
queryClient.setQueryData<Todo[]>(["todos"], (old = []) =>
old.map(t => t.id === todo.id ? { ...t, completed } : t)
);
return { previous }; // Return context for rollback
},
// If the mutation fails: roll back
onError: (err, variables, context) => {
if (context?.previous) {
queryClient.setQueryData(["todos"], context.previous);
}
},
// Always: refetch to sync with server
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ["todos"] });
},
});
return (
<div className={`flex items-center gap-3 ${mutation.isPending ? "opacity-70" : ""}`}>
<input
type="checkbox"
checked={todo.completed}
onChange={e => mutation.mutate(e.target.checked)}
/>
<span className={todo.completed ? "line-through text-gray-400" : ""}>
{todo.text}
</span>
</div>
);
}Removing an item from a list is one of the most common optimistic updates:
function CommentList({ postId }: { postId: string }) {
const queryClient = useQueryClient();
const { data: comments } = useQuery({
queryKey: ["comments", postId],
queryFn: () => fetchComments(postId),
});
const deleteMutation = useMutation({
mutationFn: (commentId: string) =>
fetch(`/api/comments/${commentId}`, { method: "DELETE" }),
onMutate: async (commentId) => {
await queryClient.cancelQueries({ queryKey: ["comments", postId] });
const previous = queryClient.getQueryData(["comments", postId]);
// Remove from cache immediately
queryClient.setQueryData<Comment[]>(["comments", postId], (old = []) =>
old.filter(c => c.id !== commentId)
);
return { previous };
},
onError: (err, commentId, context) => {
queryClient.setQueryData(["comments", postId], context?.previous);
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ["comments", postId] });
},
});
return (
<ul>
{comments?.map(comment => (
<li key={comment.id} className="flex items-start justify-between gap-4 py-3 border-b">
<p>{comment.text}</p>
<button
onClick={() => deleteMutation.mutate(comment.id)}
disabled={deleteMutation.isPending && deleteMutation.variables === comment.id}
className="text-red-500 hover:text-red-600 text-sm flex-shrink-0"
>
Delete
</button>
</li>
))}
</ul>
);
}import useSWR, { useSWRConfig } from "swr";
function LikeButton({ postId }: { postId: string }) {
const { mutate } = useSWRConfig();
async function handleLike() {
// Optimistic update — don't wait for revalidation
mutate(
`/api/posts/${postId}`,
async (current: Post) => {
// Optimistic state while request is in flight
const optimistic = {
...current,
liked: !current.liked,
likes: current.liked ? current.likes - 1 : current.likes + 1,
};
// Run the actual request
await fetch(`/api/posts/${postId}/like`, {
method: current.liked ? "DELETE" : "POST",
});
return optimistic;
},
{ optimisticData: (current: Post) => ({
...current,
liked: !current.liked,
likes: current.liked ? current.likes - 1 : current.likes + 1,
}), rollbackOnError: true }
);
}
const { data: post } = useSWR(`/api/posts/${postId}`, fetcher);
return (
<button onClick={handleLike} className={post?.liked ? "text-red-500" : "text-gray-400"}>
♥ {post?.likes ?? 0}
</button>
);
}Use optimistic updates when:
Don't use optimistic updates when:
Next lesson: Authentication sessions, JWT, and OAuth explained.
University learning companion
React & Next.js Complete Course · Optimistic Updates & Mutations
Complete the steps without looking at the answer first; record evidence rather than only a final result.
These are primary or established learning references for this subject. Check the relevant chapter or specification for the exact topic and version.
Get this course's notes on Telegram!
Free cheat sheets, summaries & practice exercises