React 19’s useOptimistic and useActionState collectively get rid of probably the most repetitive parts of that ceremony: guide loading flags, error state, and optimistic rollback logic. These two steady, first-class hooks deal with optimistic UI updates with automated rollback and type motion state administration natively, collapsing what was as soon as a wall of boilerplate into roughly 12 declarative traces.
Desk of Contents
The Boilerplate Downside in React State Administration
A typical type submission or listing mutation in React has lengthy demanded a predictable, tedious ceremony: one useState name for information, one other for loading, a 3rd for error, an async handler wrapped in strive/catch/lastly, and infrequently a useEffect for cleanup. Add optimistic UI updates and the image worsens. Builders snapshot state earlier than the mutation, apply the replace eagerly, then manually revert on failure. For a single function, this simply runs to 30 to 50 traces of mechanical plumbing.
React 19’s useOptimistic and useActionState collectively get rid of probably the most repetitive parts of that ceremony: guide loading flags, error state, and optimistic rollback logic. These two steady, first-class hooks deal with optimistic UI updates with automated rollback and type motion state administration natively, collapsing what was as soon as a wall of boilerplate into roughly 12 declarative traces.
Stipulations for the examples that observe: familiarity with React hooks, a primary understanding of async features (or server actions in Subsequent.js), and a Node.js atmosphere (Node 18 or later really helpful).
What Modified in React 19’s State Mannequin
From Guide State Machines to Declarative Actions
React 19 introduces the idea of “actions,” async features that combine instantly with React’s transition system. Somewhat than manually orchestrating state transitions throughout a number of useState and useEffect calls, builders go an async operate to React and let the framework handle pending states, serialization, and reconciliation.
Two hooks sit on the heart of this mannequin. useActionState supersedes the experimental useFormState from react-dom canary builds. Imported from react (not react-dom), it provides isPending as a 3rd return worth and manages the lifecycle of a type or crucial motion: its outcome, its error, and its pending standing. useOptimistic handles the complementary concern of exhibiting a direct UI replace that mechanically reverts as soon as the underlying async work resolves or fails.
These hooks are distinct from third-party options like React Question, SWR, or Redux Toolkit. They aim UI-local motion state, not international server cache synchronization. A mutation that wants cache invalidation throughout a number of parts nonetheless advantages from these libraries. However for the component-scoped submit-and-respond sample that dominates most purposes, the built-in hooks get rid of the necessity for exterior dependencies.
Compatibility and Adoption Notes
Each hooks require React 19.0.0 steady at least model. They work with React DOM and React Native. For Subsequent.js purposes, useActionState works with Server Actions instantly. For purely client-side purposes, any async operate works because the motion. React Native can use useActionState with crucial motion calls, however the <type motion={formAction}> sample is React DOM-specific.
To put in:
npm set up react@19 react-dom@19
Understanding useActionState
API Signature and Psychological Mannequin
Import the hook from react:
import { useActionState } from 'react';
const [state, formAction, isPending] = useActionState(actionFn, initialState, permalink?)
Three values come again. state is the collected results of the latest motion invocation, beginning as initialState. formAction is a sure operate you go on to a <type>‘s motion prop or name imperatively. isPending is a boolean that’s true whereas the motion is in flight.
This single hook replaces the frequent trio of useState calls (for outcome/error, for loading) and the strive/catch/lastly sample inside a submit handler.
This single hook replaces the frequent trio of
useStatecalls (for outcome/error, for loading) and thestrive/catch/lastlysample inside a submit handler.
Earlier than: Conventional type submission handler
import { useState } from 'react';
operate ContactForm() {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
const [isLoading, setIsLoading] = useState(false);
async operate handleSubmit(e) {
e.preventDefault();
setIsLoading(true);
setError(null);
strive {
const formData = new FormData(e.goal);
const res = await fetch('/api/contact', {
methodology: 'POST',
physique: formData,
});
if (!res.okay) throw new Error('Submission failed');
const outcome = await res.json();
setData(outcome);
} catch (err) {
setError(err.message);
} lastly {
setIsLoading(false);
}
}
return (
<type onSubmit={handleSubmit}>
<enter title="electronic mail" required />
<button disabled={isLoading}>{isLoading ? 'Sending...' : 'Ship'}</button>
{error && <p className="error">{error}</p>}
{information && <p>Thanks! We acquired your message.</p>}
</type>
);
}
After: Similar type with useActionState
The submitContact operate proven under should be outlined in the identical module (or imported) earlier than the part.
import { useActionState } from 'react';
async operate submitContact(prevState, formData) {
const electronic mail = formData.get('electronic mail');
if (!electronic mail || !/^[^s@]+@[^s@]+.[^s@]+$/.take a look at(electronic mail)) {
return { success: false, error: 'Please enter a legitimate electronic mail', information: null };
}
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 8000);
let res;
strive {
res = await fetch('/api/contact', {
methodology: 'POST',
physique: formData,
sign: controller.sign,
});
} catch (err) {
return {
success: false,
error: err.title === 'AbortError' ? 'Request timed out.' : 'Community error.',
information: null,
};
} lastly {
clearTimeout(timeoutId);
}
if (!res.okay) {
return { success: false, error: 'Server error. Please strive once more.', information: null };
}
const outcome = await res.json();
return { success: true, error: null, information: outcome };
}
operate ContactForm() {
const [state, formAction, isPending] = useActionState(submitContact, {
information: null,
error: null,
});
return (
<type motion={formAction}>
<enter title="electronic mail" required />
<button disabled={isPending}>{isPending ? 'Sending...' : 'Ship'}</button>
{state.error && !isPending && <p className="error" position="alert">{state.error}</p>}
{state.information && <p>Thanks! We acquired your message.</p>}
</type>
);
}
The various traces of state administration collapse to roughly 12 contained in the part. No onSubmit, no preventDefault, no guide loading toggle.
How the Motion Operate Works
The motion operate follows a reducer-like signature:
async (previousState, formData) => nextState
React passes the present collected state and the FormData from the shape submission. The operate returns the following state. React serializes submissions whenever you invoke actions by way of formAction or a useActionState-bound handler. React doesn’t serialize calls made exterior its transition system. The combination with <type motion={formAction}> is automated, so there is no such thing as a want for onSubmit or preventDefault.
Error Dealing with With out Attempt/Catch
As a result of the motion operate returns state moderately than throwing, error dealing with turns into a matter of returning a distinct form. The submitContact operate above demonstrates this sample: validation errors, server errors, and success all return an object that flows instantly into state. No separate error state variable, no catch block within the part.
Understanding useOptimistic
API Signature and Psychological Mannequin
The hook’s signature is:
import { useOptimistic } from 'react';
const [optimisticState, addOptimistic] = useOptimistic(state, updateFn)
The primary argument, state, is the canonical supply of fact, sometimes from props, a father or mother part’s state, or a server response. addOptimistic is a operate that triggers a direct UI replace. When the async motion wrapping the optimistic name completes (whether or not it succeeds or fails), React mechanically reconciles optimisticState again to no matter state at the moment holds.
The Automated Rollback Mechanism
The important thing perception is that useOptimistic ties its lifecycle to React’s transition system. When the React transition that triggered addOptimistic completes, optimisticState resolves again to the canonical state worth. The motion should run inside a transition — by way of <type motion>, useActionState, or express startTransition — for this rollback to happen. If the server confirmed the mutation, state will replicate the brand new information, so the optimistic replace persists naturally. If the server rejected it, state stays unchanged, and the optimistic replace vanishes. No guide snapshot, no guide revert, no cleanup results.
If the server rejected it,
statestays unchanged, and the optimistic replace vanishes. No guide snapshot, no guide revert, no cleanup results.
Earlier than: Guide optimistic replace with rollback
import { useState } from 'react';
operate TodoList({ initialTodos }) {
const [todos, setTodos] = useState(initialTodos);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
async operate addTodo(textual content) {
const snapshot = [...todos];
const tempTodo = { id: Date.now(), textual content, pending: true };
setTodos((prev) => [...prev, tempTodo]);
setIsLoading(true);
setError(null);
strive {
const res = await fetch('/api/todos', {
methodology: 'POST',
headers: { 'Content material-Sort': 'utility/json' },
physique: JSON.stringify({ textual content }),
});
if (!res.okay) throw new Error('Failed so as to add todo');
const saved = await res.json();
setTodos((prev) => prev.map((t) => (t.id === tempTodo.id ? saved : t)));
} catch (err) {
setTodos(snapshot);
setError(err.message);
} lastly {
setIsLoading(false);
}
}
return (
<div>
{error && <p className="error">{error}</p>}
<ul>{todos.map((t) => <li key={t.id}>{t.textual content}</li>)}</ul>
<button onClick={() => addTodo('New process')} disabled={isLoading}>Add</button>
</div>
);
}
After: Similar function with useOptimistic
import { useOptimistic } from 'react';
operate TodoList({ todos, addTodoAction }) {
const [optimisticTodos, addOptimisticTodo] = useOptimistic(
todos,
(currentTodos, newTodo) => [...currentTodos, newTodo]
);
async operate handleAdd(formData) {
const textual content = formData.get('textual content');
addOptimisticTodo({ id: crypto.randomUUID(), textual content, pending: true });
strive {
await addTodoAction(textual content);
} catch (err) {
console.error('Failed so as to add todo:', err);
}
}
return (
<div>
<ul>{optimisticTodos.map((t) => <li key={t.id}>{t.textual content}</li>)}</ul>
<type motion={handleAdd}>
<enter title="textual content" required />
<button kind="submit">Add</button>
</type>
</div>
);
}
The 30 traces of snapshot-and-rollback logic scale back to roughly 12. Rollback on failure is automated.
Customized Replace Capabilities
All the time present the updateFn argument — it defines merge habits. It receives (currentState, optimisticValue) and returns the brand new optimistic state. This permits builders to regulate how the optimistic worth merges: appending to an array, toggling a boolean subject, incrementing a counter, or every other transformation.
Combining Each Hooks: Full-Stack Todo Instance
Venture Setup
The next instance makes use of React 19 on the consumer and a minimal Categorical/Node.js API endpoint at POST /api/todos. The server simulates a 1-second community delay and randomly returns a 500 error roughly 30% of the time (in non-production environments), which makes it easy to watch rollback habits.
Guarantee categorical.json() middleware is registered earlier than the path to parse the JSON request physique.
Server endpoint (server.js):
const categorical = require('categorical');
const app = categorical();
const ALLOWED_ORIGIN = course of.env.ALLOWED_ORIGIN || 'http://localhost:5173';
app.use(categorical.json());
app.use((req, res, subsequent) => {
res.header('Entry-Management-Enable-Origin', ALLOWED_ORIGIN);
res.header('Entry-Management-Enable-Headers', 'Content material-Sort');
res.header('Entry-Management-Enable-Strategies', 'POST, OPTIONS');
if (req.methodology === 'OPTIONS') return res.sendStatus(204);
subsequent();
});
app.publish('/api/todos', async (req, res, subsequent) => {
strive {
const { textual content } = req.physique;
if (typeof textual content !== 'string' || textual content.trim().size === 0 || textual content.size > 500) {
return res.standing(400).json({ error: 'Invalid textual content' });
}
await new Promise((resolve) => setTimeout(resolve, 1000));
if (course of.env.NODE_ENV !== 'manufacturing' && Math.random() < 0.3) {
return res.standing(500).json({ error: 'Random server failure' });
}
const todo = { id: Date.now(), textual content: textual content.trim() };
res.json(todo);
} catch (err) {
subsequent(err);
}
});
app.hear(3000, () => console.log('Server operating on port 3000'));
Notice: In case your React dev server runs on a distinct port (e.g., 5173 for Vite), set the ALLOWED_ORIGIN atmosphere variable to match your dev server’s origin. The CORS middleware above restricts entry to a single allowed origin moderately than utilizing a wildcard, which is vital for safety on mutation endpoints.
Set up the server dependency individually:
npm set up categorical
Constructing the Part
The part under makes use of useOptimistic for immediate UI suggestions and useActionState for managing the submission lifecycle, together with pending state and error show. The motion operate returns the up to date todos listing as a part of the motion state, avoiding the concurrency hazard of calling setTodos from inside a useActionState motion.
Name addOptimisticTodo earlier than any await expression within the motion. React’s transition system solely captures optimistic updates issued synchronously earlier than the primary suspension level.
import { useOptimistic, useActionState } from 'react';
export default operate TodoList() {
async operate todoAction(prevState, formData) {
const textual content = formData.get('textual content');
const tempTodo = { id: crypto.randomUUID(), textual content, pending: true };
addOptimisticTodo(tempTodo);
let res;
strive {
res = await fetch('/api/todos', {
methodology: 'POST',
headers: { 'Content material-Sort': 'utility/json' },
physique: JSON.stringify({ textual content }),
});
} catch {
return { error: 'Community error. Please strive once more.', todos: prevState.todos };
}
if (!res.okay) {
const errBody = await res.textual content();
console.error('Todo API error:', res.standing, errBody);
return { error: 'Failed so as to add todo. Please strive once more.', todos: prevState.todos };
}
const savedTodo = await res.json();
return { error: null, todos: [...prevState.todos, savedTodo] };
}
const [state, formAction, isPending] = useActionState(todoAction, {
error: null,
todos: [],
});
const [optimisticTodos, addOptimisticTodo] = useOptimistic(
state.todos,
(present, newTodo) => [...current, newTodo]
);
return (
<div>
{state.error && !isPending && (
<p className="error" position="alert">{state.error}</p>
)}
<ul>
{optimisticTodos.map((t) => (
<li key={t.id} fashion={{ opacity: t.pending ? 0.5 : 1 }}>{t.textual content}</li>
))}
</ul>
<type motion={formAction}>
<enter title="textual content" required />
<button kind="submit" disabled={isPending}>
{isPending ? 'Including...' : 'Add Todo'}
</button>
</type>
</div>
);
}
What Occurs on Failure: Step by Step
The sequence is: the person submits the shape. useActionState wraps the motion in a React transition mechanically. The optimistic todo seems immediately within the listing at diminished opacity (pending: true). One second later, the server returns a 500 error. The motion operate returns { error: 'Failed so as to add todo. Please strive once more.', todos: prevState.todos } with the earlier todos listing unchanged. When the transition completes, React reconciles optimisticState again to the unchanged todos, and the optimistic merchandise disappears from the listing. The error message renders. Zero guide rollback code.
Implementation Guidelines and Migration Information
When to Attain for Every Hook
| Situation | Hook | Replaces |
|---|---|---|
| Type submission with loading/error | useActionState | useState x 3 + strive/catch handler |
| Instantaneous UI suggestions earlier than server confirms | useOptimistic | Guide snapshot + rollback logic |
| Each (submit + immediate suggestions) | Each collectively | 40-50 traces of customized logic |
| International server cache sync | Neither; use React Question/SWR | N/A |
Migration Guidelines
Audit
- Verify React 19.0.0 or later in
bundle.json(npm set up react@19 react-dom@19). - Determine parts with guide
isLoading/error/informationstate trios. - Determine optimistic replace patterns the place code snapshots state earlier than mutation and reverts on failure.
Exchange
- Exchange submit handlers with
useActionStatemotion features utilizing theasync (prevState, formData) => nextStatesignature. ImportuseActionStatefromreact. - Exchange
onSubmitwith<type motion={formAction}>(React DOM solely). - Take away
e.preventDefault()calls. - Exchange snapshot-and-rollback patterns with
useOptimistic, passing the canonical state as the primary argument and at all times offering anupdateFn. - Wrap optimistic calls contained in the motion operate or
startTransition. If utilizinguseActionState, the motion is already wrapped in a transition. Solely use expressstartTransitionwhen callingaddOptimisticexterior of auseActionStatemotion or type handler. - Take away guide rollback
catchblocks.
Take a look at
- Take a look at failure paths explicitly and ensure automated revert habits.
Gotchas and Limitations
Issues to Watch Out For
useActionState serializes submissions. Speedy double-clicks queue moderately than race, which prevents information corruption however means this isn’t the best software when parallel mutations are genuinely wanted.
useOptimistic solely reverts when the canonical state reference adjustments. If an motion silently fails however by no means updates the state handed to useOptimistic, the optimistic worth persists indefinitely. All the time return new state from the motion, even on failure, or make sure the canonical state variable displays the true server state.
The most typical reason for a persistent optimistic merchandise is asking addOptimistic exterior a React transition (e.g., in a plain setTimeout or a non-transition occasion handler). Guarantee all addOptimistic calls happen inside startTransition, useActionState‘s motion, or a type’s motion prop handler.
The permalink parameter in useActionState exists for progressive enhancement in server-rendered contexts (SSR/no-JS fallback) and can also be utilized by Remix/React Router v7 for type URL binding. Omit it in SPA-only purposes.
These hooks don’t change international state administration or server cache libraries. They aim component-local motion flows. For cross-component cache invalidation, server state synchronization, or background refetching, React Question, SWR, and related libraries stay the suitable selection.
These hooks don’t change international state administration or server cache libraries. They aim component-local motion flows.
Write Options, Not Plumbing
useActionState eliminates loading, error, and submission boilerplate. useOptimistic eliminates snapshot-and-rollback logic. Collectively they cowl the overwhelming majority of interactive state patterns that builders construct in part after part. Auditing one present type part and migrating it utilizing the guidelines above cuts roughly 40-50 traces of guide isLoading/error/information state administration and snapshot-based rollback logic all the way down to round 12.
The official React 19 documentation for useActionState and useOptimistic supplies further element on edge instances and superior utilization patterns.
Supply hyperlink


