React 19 use() and Actions: The New Async Model

Photo by Christiaan Colen on flickr
The use function reads the resolved value of a Promise or the current value of a Context during render. When given a pending Promise, the component suspends until it resolves, and a wrapping Suspense boundary shows a fallback in the meantime. Unlike other hooks, it can be called conditionally or inside loops, which makes it more flexible than useContext for reading Context.
This almost always happens because a new Promise is created on every render instead of being cached. React expects the same Promise instance across re-renders so it can track whether it has resolved. Fix it by creating the Promise once outside the render path, such as in a parent component, a route loader, or a Map keyed by request parameters, and passing that same instance down as a prop.
useActionState wraps an async Action function and automatically tracks its result, error, and a pending flag, replacing what used to require several separate useState calls plus manual try catch logic. It also integrates directly with the form action prop, so React resets the pending state after the action settles without extra code.
Yes. useOptimistic works with any Action, not just form submissions, including button clicks wrapped in startTransition or calls inside useActionState. The requirement is that the setter is called inside an Action, since the optimistic value only reverts automatically once that Action completes or throws.
No, adoption can be incremental. Forms and buttons with the most manual pending and error state boilerplate are the best starting point because the reduction is immediately visible and the change is isolated. Actions and use also compose with existing data libraries like React Query, since use can simply unwrap a Promise that library already produces.

Photo by Christiaan Colen on flickr
React 19 quietly rewrites the rules for how components talk to the network. For years, fetching data and submitting forms in React meant the same ritual: a useState for the result, a useState for the loading flag, a useState for the error, and a useEffect or a click handler to wire them all together. React 19 collapses most of that boilerplate into two ideas, the use function and Actions, plus a small family of hooks built around them.
This guide walks through what use actually does, how Actions change form and button handling, and where useActionState and useOptimistic fit. The goal is not to memorize an API surface but to understand the mental model well enough to know which tool to reach for the next time you build a data-driven feature, whether that is a profile form, a comment box, or a paginated list.
Despite the hook-like name, use is not a Hook in the strict sense, which is exactly what makes it powerful. It can be called inside conditionals, loops, and after early returns, something the Rules of Hooks normally forbid. What use does is simple to state and easy to misuse: hand it a Promise or a Context object, and it returns the resolved value, suspending the component while a Promise is still pending. If the promise rejects, the nearest error boundary catches it, so you no longer need a try and catch around every fetch.
function Albums({ albumsPromise }) {
// suspends until albumsPromise resolves
const albums = use(albumsPromise);
return (
<ul>
{albums.map((album) => (
<li key={album.id}>{album.title}</li>
))}
</ul>
);
}
function AlbumsPage({ albumsPromise }) {
return (
<Suspense fallback={<Spinner />}>
<Albums albumsPromise={albumsPromise} />
</Suspense>
);
}Never create the Promise you pass to use directly inside the component body on every render. A fresh Promise on each render defeats Suspense and can trigger an infinite render loop. Create the Promise once, in a parent component, a route loader, or a small cache keyed by URL, then pass the same Promise instance down as a prop.
The practical shift with use is where the loading state lives. Instead of a component tracking its own isLoading flag, the parent starts the fetch and passes the Promise down, and a Suspense boundary wrapping the consumer renders a fallback until the data resolves. This mirrors how server frameworks like Next.js already stream data, and it means the same mental model now works for client components reading Context conditionally or for Server Components streaming a Promise straight to the client. Nested Suspense boundaries also let you stream a page in pieces, showing the shell immediately while slower sections, such as a comments list or a recommendations panel, resolve independently without blocking the rest of the layout.
Because use can run inside an if statement, you can read a theme or locale Context only when a certain branch of the render actually needs it, instead of calling useContext unconditionally at the top of the function and hoping the value is not needed for other branches. This is a small change, but it removes a class of workarounds where developers used to split a component in two purely to satisfy the old Rules of Hooks around conditional Context reads.
If a fetch never changes for the lifetime of a component, memoize the Promise in a Map keyed by the request parameters. That single line of caching is usually the difference between use working cleanly and use causing a suspend loop.
An Action in React 19 is just an async function passed to a form's action prop, a button's formAction prop, or wrapped with startTransition. What makes it special is what React does around it automatically: pending state is tracked without an extra useState, the previous form values stay intact if the action throws, and errors can be caught by an error boundary. useActionState packages this pattern into a single hook that returns the current state, a wrapped action to call, and an isPending flag, effectively behaving like useReducer for side effects triggered by user interaction. The optional permalink argument also matters for progressive enhancement: if JavaScript has not finished loading yet and a user submits the form anyway, the browser can still navigate to that URL as a real HTML form submission instead of failing silently.
async function updateNameAction(previousState, formData) {
const name = formData.get("name");
const error = await updateName(name);
if (error) {
return error;
}
redirect("/profile");
return null;
}
function EditProfile() {
const [error, submitAction, isPending] = useActionState(
updateNameAction,
null
);
return (
<form action={submitAction}>
<input type="text" name="name" />
<button type="submit" disabled={isPending}>
{isPending ? "Saving..." : "Update"}
</button>
{error && <p>{error}</p>}
</form>
);
}Optimistic UI used to require careful manual bookkeeping: update local state immediately, remember the previous value, then roll back if the request fails. useOptimistic formalizes this pattern. It takes the current confirmed value and returns an optimistic value plus a setter. Call the setter synchronously inside an Action before the awaited request resolves, and React shows the optimistic value immediately, automatically reverting to the real state once the Action finishes, whether it succeeds or throws.
function ChangeName({ currentName, onUpdateName }) {
const [optimisticName, setOptimisticName] = useOptimistic(currentName);
const submitAction = async (formData) => {
const newName = formData.get("name");
setOptimisticName(newName);
const updatedName = await updateName(newName);
onUpdateName(updatedName);
};
return (
<form action={submitAction}>
<p>Your name is: {optimisticName}</p>
<input type="text" name="name" disabled={currentName !== optimisticName} />
</form>
);
}useOptimistic only reverts automatically when the Action that triggered it actually completes or throws. If you call the setter outside an Action, or if the surrounding async function never resolves because of an unhandled promise, the optimistic value can appear stuck on screen indefinitely.
With four related APIs now available, it helps to compare them side by side rather than picking one from memory. The table below lists the practical purpose of each and the situation where it is the clear choice over the alternatives. Note that useFormStatus is intentionally not a replacement for useActionState; it exists specifically so a deeply nested submit button can read the pending status of its parent form without that status being threaded down as a prop.
| API | Purpose | Use it when |
|---|---|---|
| use() | Unwraps a Promise or reads Context during render, suspending until ready | Reading data passed down as a cached Promise, or reading Context conditionally |
| useActionState | Wraps an async Action and tracks its result, error, and pending flag | Handling a form submission or button click that calls a server mutation |
| useOptimistic | Shows an immediate UI update that reverts automatically if the Action fails | Likes, follows, chat messages, or any interaction where instant feedback matters more than certainty |
| useFormStatus | Reads the pending status of the nearest parent form from a child component | A submit button or spinner lives in a separate component from the form itself |
Adopting these APIs works best as an incremental migration rather than a rewrite. Start with the forms and buttons that already have the most manual pending and error state, since that is where the boilerplate reduction is most visible and the risk is lowest. There is no requirement to touch data fetching and form handling in the same pass; treating them as two separate migrations keeps each pull request small enough to review carefully and roll back independently if something regresses.
Teams that migrated form-heavy pages first reported the biggest drop in boilerplate, because useActionState alone typically removes three separate useState calls per form.
Actions and use compose well with React Query or SWR too. Nothing about this model requires abandoning your existing data layer; use can unwrap a Promise that a query library already produced and cached.