Concept
The beginner framing: React 19 adds a set of features, Actions, useOptimistic, and a build-time compiler, aimed at removing boilerplate that used to require manual state wiring for common patterns like form submission and optimistic UI updates.
The precise mental model: each of these features takes something you'd previously hand-build with useState and useEffect, pending/error tracking around an async operation, an optimistic UI update with a manual revert path, memoization to avoid unnecessary re-renders, and gives React enough structure to manage it automatically.
Actions: pending/error state for an async operation, without hand-wiring it
function ChangeNameForm({ currentName }) {
const [error, submitAction, isPending] = useActionState(
async (previousState, formData) => {
const result = await updateName(formData.get("name"));
if (result.error) return result.error; // returned value becomes the next `error` state
return null;
},
null // initial error state
);
return (
<form action={submitAction}>
<input name="name" defaultValue={currentName} />
<button disabled={isPending}>Update</button>
{error && <p>{error}</p>}
</form>
);
}Passing submitAction directly to the form's action prop wires up submission handling, automatic isPending tracking during the async call, and a way to feed the action's return value back as the next state, all without a hand-rolled useState for loading and a separate one for error, plus the manual try/catch wiring that pattern usually needs.
useOptimistic: show the expected outcome immediately, revert if it doesn't happen
function LikeButton({ likes, postId }) {
const [optimisticLikes, addOptimisticLike] = useOptimistic(
likes,
(currentLikes) => currentLikes + 1
);
async function handleLike() {
addOptimisticLike(); // UI updates INSTANTLY, before the server responds
await likePost(postId); // if this fails, React reverts to the real `likes` value automatically
}
return <button onClick={handleLike}>{optimisticLikes} likes</button>;
}The displayed count jumps immediately on click, hiding network latency, and if likePost ultimately fails, React automatically reverts the optimistic value back to the real, server-confirmed likes, without any manual rollback code.
The React Compiler: automating what Performance teaches by hand
The React Compiler analyzes component code at build time and automatically inserts the equivalent of useMemo/useCallback/React.memo wherever it can safely determine doing so is correct, reducing the need to manually reach for those tools everywhere Performance Optimization covers, for code that already follows React's rules (pure render, correct dependency usage). It doesn't replace understanding when and why memoization helps, it automates the mechanical, error-prone parts (like getting a dependency array exactly right).
Try It
Predict what happens before checking the solution.
function LikeButton({ likes, postId }) {
const [optimisticLikes, addOptimisticLike] = useOptimistic(likes, (c) => c + 1);
async function handleLike() {
addOptimisticLike();
await likePost(postId); // this call will REJECT in this scenario
}
return <button onClick={handleLike}>{optimisticLikes} likes</button>;
}If likePost rejects, what does the button display right after the rejection, versus right after the click?
Solution
Right after the click, the button instantly shows the incremented count (the optimistic value), before the server has responded at all. Once likePost rejects, React automatically discards the optimistic value and the button reverts to displaying the real likes prop, since the optimistic state was never actually confirmed. No manual rollback code is needed, reverting on failure is useOptimistic's built-in behavior when the underlying action doesn't succeed.
Implement It Yourself
Build a simplified useOptimistic to see exactly how the "show immediately, revert on failure" behavior works:
function useSimpleOptimistic(baseState, updateFn) {
const [optimisticValue, setOptimisticValue] = useState(null);
function addOptimistic(action) {
setOptimisticValue(updateFn(optimisticValue ?? baseState, action));
}
function reset() {
setOptimisticValue(null); // clears the override, falls back to the REAL baseState
}
return [optimisticValue ?? baseState, addOptimistic, reset];
}
// Usage:
const [likes, addOptimisticLike, resetOptimistic]
The real useOptimistic automates exactly this pattern, an override value shown instead of the real one, automatically cleared once the real state catches up (on success) or explicitly, on failure.
Under the Hood
Actions are the Actions-and-useActionState evolution of the manual controlled-form pattern from Forms, instead of hand-wiring useState for the submitted value, a loading flag, and an error message around a handleSubmit, the framework provides that same shape as a built-in contract. And the React Compiler's entire premise rests on the same guarantees Rendering Lifecycle and Performance Optimization already established, a render must be a pure calculation with correctly-tracked dependencies, the compiler is only able to safely auto-memoize because well-formed React code already promises those properties; it's automating a mechanical process, not inventing new semantics.
Common Mistakes
1. Assuming Actions eliminate the need to design pending/error UI
useActionState gives you isPending and a returned state value automatically, it doesn't design the fallback UI for you. A form still needs a deliberate loading indicator and error display; Actions just remove the manual state-wiring boilerplate around producing those values correctly.
2. Forgetting an optimistic update can still fail and must be reconciled
addOptimisticLike(); // shows the expected outcome immediately
await likePost(postId); // ❌ if this throws and isn't handled, the user may see inconsistent statesAn optimistic update is a guess about the outcome, the underlying action can still fail, and the calling code needs to actually await it and handle rejection (even if useOptimistic itself reverts the displayed value automatically, the surrounding logic may still need to surface an error message to the user).
3. Assuming the React Compiler removes all need for manual memoization
The Compiler automates safe, mechanical memoization for code that already follows React's rules, it isn't a substitute for understanding when memoization helps (see Performance Optimization), and there remain cases (values crossing certain boundaries, code the compiler can't safely analyze) where manual useMemo/useCallback/memo are still necessary.
Best Practices
- Reach for
useActionStatefor form-submission-shaped async flows instead of hand-wiringisPending/error state withuseStateand a manualtry/catch. - Reserve
useOptimisticfor latency-sensitive, usually-successful interactions (likes, sends, toggles) where showing the expected outcome immediately, and occasionally reverting, is an acceptable, even desirable, tradeoff for perceived speed. - Write idiomatic, rules-following React code rather than working around the Compiler, it optimizes code that already respects render purity and correct dependencies; fighting it or disabling it defeats its purpose.
- Still design explicit loading and error states even when Actions handle the state-tracking mechanics, the UI for those states is still a deliberate design decision.
Performance Tips
- Optimistic UI doesn't make the underlying network request faster, it hides the latency perceptually, which is a real UX win but not a network optimization.
- The React Compiler reduces manual memoization boilerplate and the class of bugs that come from a wrong or incomplete dependency array, but profiling (see Performance Optimization) is still the right way to confirm a component's actual behavior, compiler-optimized or not.
