Optimistic UI updates the interface before the server confirms a change, making interactions feel faster and more responsive.
Imagine a user checks a todo item. If the UI waits for the server response before marking it complete, the interaction feels slow.
Instead, update the UI immediately and synchronize the change with the server in the background.
Benefits:
useOptimistic WorksReact’s useOptimistic lets you display a temporary optimistic state while an Action is pending.
const [optimisticState, setOptimisticState] =useOptimistic(currentState);
The optimistic value can temporarily differ from the underlying state. When the Action finishes, React returns to the current value supplied by the parent unless that underlying state has also been updated.
import { useOptimistic } from "react";function Todo({ todo, updateTodo }) {const [optimisticComplete, setOptimisticComplete] =useOptimistic(todo.isComplete);return (<formaction={async () => {const nextComplete = !optimisticComplete;setOptimisticComplete(nextComplete);await updateTodo(todo.id, nextComplete);}}><button type="submit">{optimisticComplete ? "✓" : "○"} {todo.text}</button></form>);}
The button provides immediate visual feedback, while the form Action synchronizes the change with the server.
When the Action finishes, the optimistic value stops being active. The parent must update todo.isComplete with the confirmed server state so the UI remains consistent.
Important: If the request fails, the optimistic UI reverts when the Action ends, provided the underlying state was never changed to the optimistic value. You should still handle the error and inform the user.
When creating a record, you can display a temporary version before the server returns the real one.
function PokemonForm({ createPokemon }) {const [optimisticPokemon, setOptimisticPokemon] =useOptimistic(null);return (<formaction={async (formData) => {const name = String(formData.get("name") ?? "");setOptimisticPokemon({ name, pending: true });await createPokemon(formData);}}><input name="name" required placeholder="Pokémon name" /><button type="submit">Create Pokémon</button>{optimisticPokemon && (<p>{optimisticPokemon.name}{optimisticPokemon.pending ? " (Saving...)" : ""}</p>)}</form>);}
This is a simplified example. A production implementation should reconcile the optimistic item with the returned record and handle failures. If the parent owns the list of Pokémon, update that underlying list after the server confirms the creation.
For a workflow with multiple steps, such as creating, saving, and notifying, ordinary state is often clearer for progress messages.
function CreateForm() {const [status, setStatus] = useState("Create");async function handleCreate(formData) {try {setStatus("Creating...");const pokemon = await createPokemon(formData);setStatus("Saving...");await savePokemon(pokemon);setStatus("Finalizing...");await notifyFleet(pokemon);setStatus("Complete");} catch {setStatus("Failed. Please try again.");}}return (<form action={handleCreate}><input name="name" required /><button type="submit">{status}</button></form>);}
Here, useState is appropriate because the status represents actual workflow progress, not merely a temporary optimistic version of the data.
useOptimistic?Use it for actions where immediate feedback is valuable, such as liking a post, toggling a todo, adding a comment, or creating a list item.