Follow AiTechWorlds on LinkedIn for professional AI content!Follow Now →
18 minLesson 24 of 33
Data Fetching & Caching

Optimistic Updates & Mutations

Optimistic Updates & Mutations

An optimistic update changes the UI immediately — before the server confirms the action. When a user clicks "Like", the like count jumps instantly. The server request runs in the background. If the server fails, the UI rolls back. The result feels instant even on slow connections.

The Pattern

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 error

Optimistic Updates with useOptimistic (React 19)

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

Optimistic Updates with TanStack Query

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

Optimistic Delete

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

SWR Optimistic Updates

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

When to Use Optimistic Updates

Use optimistic updates when:

  • The action almost always succeeds (toggling a like, checking a todo)
  • The visual feedback is important (user should feel instant response)
  • Rolling back is simple (one record changes)

Don't use optimistic updates when:

  • The action can fail for legitimate reasons the user needs to know about first (payment, age verification)
  • The new state depends on server computation you can't predict (generating an ID, calculating a price)
  • Rollback is complex or would be confusing to the user

Next lesson: Authentication — sessions, JWT, and OAuth explained.

📱

Get this course's notes on Telegram!

Free cheat sheets, summaries & practice exercises

Get Notes Free →
!