# useOptimistic

## 1. The Problem It Solves

When a user sends a chat message or likes a post, waiting for the server round-trip before showing the change makes the UI feel sluggish — the "correct" data is often extremely predictable, so showing it immediately (and quietly correcting course if the server disagrees) feels far more responsive:

```tsx
// Without useOptimistic: the new message only appears once the network
// request resolves — a visible delay between clicking "Send" and seeing it
function ChatThread({ messages, sendMessage }: { messages: Message[]; sendMessage: (text: string) => Promise<void> }) {
  const [pending, setPending] = useState(false);

  async function handleSend(text: string) {
    setPending(true);
    await sendMessage(text); // messages only updates after this resolves
    setPending(false);
  }
  // ... the sent message is invisible until the round-trip completes
}
```

`useOptimistic` lets the UI display a *predicted* state immediately, while the real update is still in flight, and automatically reverts to the real data once the actual update lands (or fails).

## 2. The Basic API

```tsx
import { useOptimistic } from "react";

interface Message {
  id: string;
  text: string;
  sending?: boolean;
}

function ChatThread({ messages, sendMessage }: { messages: Message[]; sendMessage: (text: string) => Promise<void> }) {
  const [optimisticMessages, addOptimisticMessage] = useOptimistic(
    messages,
    (currentMessages, newText: string) => [
      ...currentMessages,
      { id: `temp-${Date.now()}`, text: newText, sending: true },
    ]
  );

  async function handleSend(formData: FormData) {
    const text = formData.get("text") as string;
    addOptimisticMessage(text); // shows immediately, before the network call
    await sendMessage(text);    // once this resolves, `messages` (the real
                                  // prop) updates and optimisticMessages
                                  // reverts to reflecting it
  }

  return (
    <form action={handleSend}>
      {optimisticMessages.map((m) => (
        <p key={m.id} style={{ opacity: m.sending ? 0.5 : 1 }}>{m.text}</p>
      ))}
      <input name="text" />
    </form>
  );
}
```

`useOptimistic(actualState, updateFn)` returns `[optimisticState, addOptimistic]`:

- `actualState` — the real, confirmed state (usually a prop derived from server data).
- `updateFn(currentState, optimisticValue) => newState` — a pure function describing how to merge an optimistic guess into the current (possibly already-optimistic) state.
- `optimisticState` — what to render; equals `actualState` when nothing is pending, or the predicted merge while a transition is in flight.
- `addOptimistic(value)` — call this (inside a transition — typically inside a form action or another transition-wrapped handler) to apply the optimistic update immediately.

## 3. It Automatically Reverts

The optimistic value is never stored as its own independent state that a developer must remember to clear — it's derived fresh every render from `actualState` and whatever optimistic values are still pending:

```tsx
// If sendMessage fails, don't manually "undo" the optimistic message —
// simply don't update `messages` with it. Once the pending transition
// settles, useOptimistic re-derives optimisticState from the real
// `messages` prop, and the temporary optimistic entry disappears on its own.
async function handleSend(formData: FormData) {
  const text = formData.get("text") as string;
  addOptimisticMessage(text);
  try {
    await sendMessage(text); // on success, caller re-fetches/updates `messages`
  } catch {
    showErrorToast("Message failed to send"); // no manual optimistic-state cleanup needed
  }
}
```

This is the core mental model: `optimisticState` is *always* a function of `actualState` plus in-flight guesses, never a separate mutable store — so there's nothing to "roll back" by hand.

## 4. Must Be Called Inside a Transition

`addOptimistic(value)` only has an effect when called during a **transition** — inside a `useActionState`/form action, inside `startTransition`, or inside another hook that's already part of one. Calling it in a plain event handler outside any transition does nothing useful, because React needs a transition to know it should render the optimistic value *first*, then keep that render around while the real update resolves:

```tsx
// CORRECT: inside a form action (which useActionState/form actions
// automatically wrap in a transition)
<form action={async (formData) => {
  addOptimisticMessage(formData.get("text") as string); // works — inside the action
  await sendMessage(formData.get("text") as string);
}}>

// Also correct: explicitly wrapped
function handleClick() {
  startTransition(() => {
    addOptimisticLike();
  });
}
```

## 5. Composed With useActionState

The two are frequently paired: `useActionState` manages the actual async submission and its result, while `useOptimistic` predicts what that result will look like before it arrives:

```tsx
async function likePostAction(previousState: number, _formData: FormData) {
  await new Promise((r) => setTimeout(r, 800)); // network latency
  return previousState + 1; // the server-confirmed like count
}

function LikeButton({ initialLikes }: { initialLikes: number }) {
  const [likes, formAction] = useActionState(likePostAction, initialLikes);
  const [optimisticLikes, addOptimisticLike] = useOptimistic(likes, (current) => current + 1);

  return (
    <form action={async (formData) => {
      addOptimisticLike(undefined); // instantly shows likes + 1
      await formAction(formData);   // real update lands in `likes` once resolved
    }}>
      <button>{optimisticLikes} likes</button>
    </form>
  );
}
```

![useOptimistic renders a predicted merge of actualState immediately; once the real update commits, actualState changes and optimisticState re-derives from it, discarding the temporary guess](diagrams/01-useoptimistic-revert.svg)

## 6. Under the Hood

React does not attach state to the component *function* — a function component is just a function, called fresh every render with no persistent memory of its own. Hook data lives on the **fiber**, in the same **singly linked list of hook objects** every hook in this curriculum uses, walked in strict call order every render.

`useOptimistic`'s hook node tracks the passed-in `actualState` and a pending queue of optimistic updates, conceptually similar to how `useState`'s node tracks a pending-update queue. What's different is *when* the optimistic value gets rendered versus committed: calling `addOptimistic(value)` inside a transition tells React "re-render right now, showing the result of `updateFn(latestState, value)`, but treat this render as provisional." React shows that provisional render to the user immediately — this is what makes the UI feel instant — while the transition that triggered it (an async form action, typically) is still running in the background.

The "revert" behavior is not an undo operation React performs — it's a direct consequence of `optimisticState` being **recomputed from scratch every render**, as `updateFn(actualState, pendingOptimisticValue)`, rather than being its own independently stored value. Once the transition's real async work resolves and updates `actualState` (by whatever mechanism the surrounding code uses — a `useActionState` result, a state setter, a re-fetch), the next render calls `updateFn` again with the *new* `actualState`. If there's no longer a pending optimistic value queued for that update, `optimisticState` simply equals `actualState` — the temporary guess evaporates because it was never persisted anywhere; it only existed as one render's derived output. This is exactly analogous to how `useMemo`'s cached value is derived, not stored independently of its inputs — except here the "inputs" include a transient in-flight value tracked by the transition system, not just a dependency array.

This ties `useOptimistic` structurally to the same transition machinery `useActionState` and `useTransition` use: all three are built around React tracking "work that's allowed to keep the current UI interactive while something asynchronous resolves in the background," rather than the older model of a single synchronous state update per event.

## 7. Common Pitfalls

1. **Calling `addOptimistic` outside a transition** (a plain click handler with no `startTransition`, no form action) — the optimistic update has no transition to attach to and won't behave as expected.
2. **Treating `optimisticState` as separately mutable state** — it's derived from `actualState` every render; there's no "setOptimisticState" to call directly outside `updateFn`'s merge logic.
3. **Forgetting to eventually update `actualState`** — if the real async operation never updates the underlying state/prop, the optimistic value has nothing to revert to or reconcile against once the transition settles, leaving the UI showing a stale prediction indefinitely.
4. **Writing an impure or side-effecting `updateFn`** — like a reducer, it should be a pure function from `(currentState, optimisticValue)` to a new state, called potentially multiple times as renders occur.
5. **Using `useOptimistic` for updates where an incorrect guess would be confusing or costly to show** (e.g., a payment total) — it's best suited to low-stakes, highly predictable updates (likes, sent messages, toggles) where a brief incorrect flash is an acceptable trade for responsiveness.

## 8. Quick Reference

```
const [optimisticState, addOptimistic] = useOptimistic(actualState, updateFn)

updateFn: (currentState, optimisticValue) => newState   → pure, like a reducer

addOptimistic(value)  → call INSIDE a transition (form action / startTransition)
optimisticState        → actualState merged with any pending optimistic guess
                          → automatically reverts once actualState updates for real
```
