In React 19, what are the two arguments to useOptimistic, what does it return, and at what moment does the optimistic value stop being rendered?
answer
- two arguments, reducer-shaped
- the second is a pure merge function
- the returned updater is not a setter
- the layer lives only during the action
- dropped when the action settles, either way
basics
~10 suseOptimistic(state, updateFn) returns [optimisticState, addOptimistic]. Calling addOptimistic(value) inside an action immediately renders updateFn(currentState, value); when that action settles React drops the optimistic layer and renders the real state argument again.
solid answer
~40 s`useOptimistic` takes the real state you already have and a pure update function, and returns `[optimisticState, addOptimistic]`. While nothing is in flight, `optimisticState` is just the `state` you passed in. Calling `addOptimistic(value)` — which must happen inside an action or transition — makes React render `updateFn(currentState, value)` right away, before any server round trip. The moment the surrounding action settles, React throws the optimistic layer away and renders whatever the `state` argument is at that point. That single rule explains both outcomes: if the action succeeded and the real state now includes the new item, the UI does not flicker; if it failed and the state never changed, the UI reverts on its own. There is no rollback code to write, and `updateFn` must be pure because React may call it more than once.
code
jsx · 28 linesimport { useOptimistic } from 'react';
export function Thread({ messages, sendMessage }) {
const [optimisticMessages, addOptimisticMessage] = useOptimistic(
messages,
(current, text) => [...current, { id: `pending-${text}`, text, sending: true }]
);
async function send(formData) {
const text = formData.get('text');
addOptimisticMessage(text);
await sendMessage(text);
}
return (
<>
<ul>
{optimisticMessages.map((m) => (
<li key={m.id} style={{ opacity: m.sending ? 0.5 : 1 }}>{m.text}</li>
))}
</ul>
<form action={send}>
<input name="text" />
<button type="submit">Send</button>
</form>
</>
);
}go deeper
Know the destructuring const [optimisticValue, addOptimistic] = useOptimistic(realValue, updateFn) and that you render the optimistic value while calling the updater inside the form action.
Explain that updateFn is reducer-shaped and pure, that the optimistic value appears immediately without awaiting anything, and that React drops it when the action settles — which is why no rollback code exists.
Show that you check the real state actually updates on success, since the layer is discarded either way; and that you mark provisional items in the update function so users can tell confirmed data from in-flight data.
Be ready to argue where optimistic UI is worth its complexity: it suits high-success, low-stakes, easily-reversed writes, and is a poor trade for irreversible or money-moving operations where showing a false success costs more than a spinner does.
## The signature ```jsx const [optimisticState, addOptimistic] = useOptimistic(state, updateFn); ``` - **`state`** — the real, confirmed value. Usually props, or state from another hook. `useOptimistic` never owns it. - **`updateFn(currentState, optimisticValue)`** — a pure function that merges an optimistic value into the current state and returns the result. It has the same shape as a reducer. - **`optimisticState`** — what you render. Equal to `state` when nothing is pending; equal to the merged result while an action is in flight. - **`addOptimistic(value)`** — call it with whatever you want merged in. React passes it to `updateFn` as the second argument. ```jsx const [optimisticMessages, addOptimisticMessage] = useOptimistic( messages, (current, newText) => [...current, { text: newText, sending: true }] ); ``` ## The three moments **Before.** No action running, so `optimisticState === state`. The hook is inert. **During.** Inside an action you call `addOptimistic(value)`. React re-renders immediately with `updateFn(state, value)` — no await, no server. If you call it several times before the action settles, React folds each value through `updateFn` in turn, so the results accumulate rather than replacing each other. **After.** The moment the action settles — resolved *or* rejected — React discards the optimistic layer entirely and renders the `state` argument as it is then. This is the sentence to say in an interview, because it explains both the happy and unhappy path with one mechanism: - The action succeeded and the confirmed state now contains the item ⇒ the optimistic row is replaced by the real row, and the user sees no flicker. - The action failed, or succeeded but the confirmed state was never updated ⇒ the optimistic row simply vanishes. That is the "rollback", and you wrote no code for it. A consequence people miss: the optimistic value is dropped whether or not the request succeeded. If the server accepted the write but your real state never picked up the new value, the UI still reverts and looks like a failure. The optimistic layer is a temporary overlay, never a place to store results. ## Where addOptimistic may be called It must run inside an action or a transition — typically the first line of a form action, before the `await`: ```jsx async function send(formData) { const text = formData.get('text'); addOptimisticMessage(text); // instant, still inside the action await api.send(text); // the pending window } <form action={send}> <input name="text" /> <button type="submit">Send</button> </form> ``` Called outside that window there is no pending action for React to tie the optimistic value to, so it has nothing to revert at and React warns about it. The rule follows from the mechanism: the optimistic layer exists exactly for the duration of an action. ## Why updateFn must be pure React may call it more than once, and calls it during rendering. Mutating `current` instead of returning a new value, pushing into an array in place, or issuing a side effect inside it will produce wrong or duplicated UI. Treat it exactly as you would a reducer: derive and return, never mutate. ```jsx // Wrong: mutates the confirmed state array. (current, text) => { current.push({ text }); return current; } // Right: returns a new array. (current, text) => [...current, { text, sending: true }] ``` ## Marking the provisional item Because the optimistic entry is indistinguishable from a confirmed one unless you say so, `updateFn` is where you add the marker — a `sending: true` flag, a temporary id, a reduced opacity. Rendering that marker is what turns "instant but a lie" into "instant and honest". ## What it is not `useOptimistic` is not a cache and not a state manager. It holds no value between actions, it cannot be read outside the component that called it, and it does not retry anything. It is a render-time overlay whose entire lifetime is one action. Anything you need after the action settles has to live in the real state you passed in as the first argument.
- An action succeeds but the list still snaps back to its old contents. What is wrong?The confirmed state never changed. React discards the optimistic layer when the action settles and renders the `state` argument as it is at that moment, so if your real data source was not refreshed or updated with the new item, the UI reverts even though the server accepted the write. Fix the real state update, not the optimistic hook.
- What happens if addOptimistic is called twice before the action settles?React folds both values through `updateFn` in sequence — the second call receives the already-optimistic state as `currentState` — so the effects accumulate rather than the second replacing the first. That is why the update function must be written as a pure merge of one value into a current state rather than as a whole-state setter.
- Why must the update function be pure rather than, say, pushing into the existing array?React calls it during rendering and may call it more than once for the same value. Mutating the array you were handed corrupts the confirmed state and can duplicate entries on a repeat call. Return a new value derived from the arguments, exactly as you would in a reducer, and put any side effects in the action itself.
saying these in an interview costs you the question
- Treats the returned updater as a plain state setter
- Writes explicit rollback code for the failure path
- Calls the updater outside an action or transition
- Mutates the current state inside the update function
- Expects the optimistic value to persist after the action settles