Skip to solution
hardFrontend

How does `useOptimistic` enable optimistic UI in React 19?

704 views
01

Understand the problem

Question presented to candidate: "A like button should feel instant even though the request takes 300ms. How does useOptimistic help, and what happens if the request fails?"

What a strong answer should cover:

  • useOptimistic(actualState, updateFn) returns a value that may differ from real state while an action is pending, plus a function to apply an optimistic update.
  • It shows a predicted result immediately, then automatically reverts to the real state when the transition settles.
  • Error handling is the headline benefit: if the action throws, React discards the optimistic value automatically. No manual rollback code.
  • It must be used inside a transition — an action, or startTransition. Outside one, the optimistic value is discarded immediately.
  • The update function is a reducer: (currentState, optimisticValue) => newState, so several optimistic updates compose.
  • Pairs naturally with form actions and useActionState.
  • The distinction from plain local state: hand-rolled optimistic UI means writing the revert path yourself, and getting it wrong on error or on rapid repeated actions.
  • Judgement: optimistic UI suits high-success, low-stakes actions — likes, toggles, reordering. Not payments.

Clarifying questions expected:

  • "How likely is this action to fail, and how bad is a visible revert?" — that decides whether optimistic UI is appropriate at all.
  • "Are we already using form actions?"

Code / implementation expected: Yes — an optimistic list or counter, including a failure path.

reactreact-19hooksoptimistic
02

Attempt it yourself

Sketch your approach before reading the solution — that's what interviews test.

Nudge consolestandby

Stuck? Beam a request up — the console returns a conceptual nudge that guides your logic without spoiling the implementation.

03

Study the solution

Target Audience: Engineers preparing for senior React interviews — assumes transitions and actions. Difficulty: Hard

How to read this doc: Concepts are explained in plain language first, then tagged with 📌 Interview term:. The timing behaviour in section 3 was produced by clicking a real bu

Solution ready — 2 min read

Classified // press E to declassify

04

Explore the playground snippets

An optimistic list with a deliberate failure path
Run Playground
import { useState, useOptimistic, useTransition } from "react";

let nextId = 3;
const wait = (ms) => new Promise((r) => setTimeout(r, ms));

export default function App() {
  const [todos, setTodos] = useState([
    { id: 1, text: "Read the docs", pending: false },
    { id: 2, text: "Try useOptimistic", pending: false },
  ]);
  const [failNext, setFailNext] = useState(false);
  const [error, setError] = useState(null);
  const [isPending, startTransition] = useTransition();

  // The reducer receives the CURRENT real state and the optimistic value, so
  // several pending additions stack on top of each other.
  const [optimisticTodos, addOptimistic] = useOptimistic(
    todos,
    (current, newTodo) => [...current, newTodo],
  );

  const add = (text) => {
    setError(null);
    // Must be inside a transition — that is the pending window the optimistic
    // value lives in. Outside one it would be dropped immediately.
    startTransition(async () => {
      addOptimistic({ id: "temp-" + Date.now(), text, pending: true });

      await wait(900);                              // pretend network

      if (failNext) {
        // No rollback code needed: throwing discards the optimistic entry.
        setError("Server rejected: " + text);
        setFailNext(false);
        throw new Error("rejected");
      }
      setTodos((prev) => [...prev, { id: nextId++, text, pending: false }]);
    });
  };

  return (
    <div style={{ padding: 24, fontFamily: "system-ui", lineHeight: 1.6, maxWidth: 480 }}>
      <form
        onSubmit={(e) => {
          e.preventDefault();
          const text = new FormData(e.target).get("text");
          if (text) { add(text); e.target.reset(); }
        }}
      >
        <input name="text" placeholder="add a todo" defaultValue="New task" />{" "}
        <button type="submit">add</button>
      </form>

      <label style={{ display: "block", margin: "8px 0", fontSize: 14 }}>
        <input type="checkbox" checked={failNext} onChange={(e) => setFailNext(e.target.checked)} />{" "}
        make the next request fail
      </label>

      <ul style={{ paddingLeft: 20 }}>
        {optimisticTodos.map((t) => (
          <li key={t.id} style={{ opacity: t.pending ? 0.5 : 1, fontStyle: t.pending ? "italic" : "normal" }}>
            {t.text} {t.pending && <span style={{ fontSize: 12, color: "#666" }}>(saving…)</span>}
          </li>
        ))}
      </ul>

      {error && (
        <p style={{ color: "crimson", fontSize: 14 }}>
          {error} — note the item vanished from the list with no rollback code.
        </p>
      )}

      <p style={{ color: "#666", fontSize: 13 }}>
        Add an item: it appears instantly, dimmed, while the request runs. Tick
        the failure box and add another — it appears, then disappears when the
        action throws. React discarded the optimistic entry on its own.
        {isPending && <strong> (a request is in flight)</strong>}
      </p>
    </div>
  );
}
05

Join the discussion

Discussion (0)

Sign in to join the discussion.

No responses yet. Be the first to share what you think.

Transmission complete // awaiting log

KEEP THE
STREAK ALIVE.

Dossier 90 of 119 decoded in the React.js track. One more won't hurt.

Back to track