typescriptadvanced

React Optimistic Update Pattern

Implement optimistic UI updates that show changes immediately and rollback on server failure.

typescript
'use client';
import { useState, useCallback, useTransition } from 'react';

interface Todo {
  id: string;
  text: string;
  completed: boolean;
}

// Simulated API
async function apiToggleTodo(id: string): Promise<Todo> {
  const res = await fetch(`/api/todos/${id}/toggle`, { method: 'PATCH' });
  if (!res.ok) throw new Error('Failed to update');
  return res.json();
}

async function apiDeleteTodo(id: string): Promise<void> {
  const res = await fetch(`/api/todos/${id}`, { method: 'DELETE' });
  if (!res.ok) throw new Error('Failed to delete');
}

function useTodos(initial: Todo[]) {
  const [todos, setTodos] = useState<Todo[]>(initial);
  const [isPending, startTransition] = useTransition();

  const toggleTodo = useCallback(async (id: string) => {
    // Optimistic update
    const previous = todos;
    setTodos((prev) =>
      prev.map((t) => (t.id === id ? { ...t, completed: !t.completed } : t))
    );

    try {
      const updated = await apiToggleTodo(id);
      setTodos((prev) => prev.map((t) => (t.id === id ? updated : t)));
    } catch {
      // Rollback on failure
      setTodos(previous);
    }
  }, [todos]);

  const deleteTodo = useCallback(async (id: string) => {
    const previous = todos;
    setTodos((prev) => prev.filter((t) => t.id !== id));

    try {
      await apiDeleteTodo(id);
    } catch {
      setTodos(previous);
    }
  }, [todos]);

  return { todos, toggleTodo, deleteTodo, isPending };
}

// Component
function TodoList({ initial }: { initial: Todo[] }) {
  const { todos, toggleTodo, deleteTodo } = useTodos(initial);

  return (
    <ul>
      {todos.map((todo) => (
        <li key={todo.id} className="flex items-center gap-2 p-2">
          <input
            type="checkbox"
            checked={todo.completed}
            onChange={() => toggleTodo(todo.id)}
          />
          <span className={todo.completed ? 'line-through text-gray-500' : ''}>
            {todo.text}
          </span>
          <button
            onClick={() => deleteTodo(todo.id)}
            className="ml-auto text-red-400"
          >
            Delete
          </button>
        </li>
      ))}
    </ul>
  );
}

Use Cases

  • Instant UI feedback for server operations
  • Todo and task list toggle/delete patterns
  • Like/bookmark with automatic rollback

Tags

Related Snippets

Similar patterns you can reuse in the same workflow.