The theme of React 19 is that the boilerplate around async work — a pending flag, an error state, an optimistic update, a form reset — became part of the framework. Actions and the hooks around them replace a pattern nearly every React codebase had reimplemented.
There is a lot in the release. Most of it is worth reading about once and forgetting until you need it. These are the parts that change how you write ordinary components, rather than the parts that change what a framework author has to think about.
Table of contents
- Actions, and the state you stop writing
- useOptimistic, done properly
- The use API, and reading context conditionally
- The quieter changes that remove real friction
- Upgrading, and what to expect
- How this fits the rest of the stack
- FAQ
Actions, and the state you stop writing
Almost every form in every React codebase contained the same four pieces of state. React 19 names the pattern — a function that uses an async transition is an Action — and gives you a hook that manages it.
// Before: the boilerplate everyone wrote
function UpdateName() {
const [name, setName] = useState('');
const [error, setError] = useState(null);
const [isPending, setIsPending] = useState(false);
async function handleSubmit(e) {
e.preventDefault();
setIsPending(true);
setError(null);
try {
await updateName(name);
} catch (err) {
setError(err.message);
} finally {
setIsPending(false);
}
}
// ...
}
// After: useActionState handles pending, error and result
import { useActionState } from 'react';
function UpdateName() {
const [state, submitAction, isPending] = useActionState(
async (previous, formData) => {
const error = await updateName(formData.get('name'));
if (error) return { error };
redirect('/profile');
return { error: null };
},
{ error: null }
);
return (
<form action={submitAction}>
<input name="name" />
<button disabled={isPending}>Save</button>
{state.error && <p role="alert">{state.error}</p>}
</form>
);
}
The action prop on a form is the other half. Passing a function rather than a URL means React handles submission, manages the pending state, and resets the form automatically on success. formAction does the same on individual buttons, which makes multiple submit actions on one form straightforward.
Two smaller wins fall out of this. The uncontrolled input with a name attribute is back as a reasonable default, so a form with a dozen fields no longer needs a dozen pieces of state. And because the pending state comes from the framework, it is correct across the whole submission rather than approximately correct.
useFormStatus reads the parent form’s pending state from a child component, so a submit button knows the form is busy without prop drilling.
useOptimistic, done properly
Optimistic updates — showing the result before the server confirms — were previously a manual exercise in applying a change and remembering how to undo it if the request failed. Getting the rollback right was the hard part, and it was usually the part that was wrong.
import { useOptimistic } from 'react';
function Messages({ messages, sendMessage }) {
const [optimistic, addOptimistic] = useOptimistic(
messages,
(current, newMessage) => [...current, { text: newMessage, sending: true }]
);
async function action(formData) {
const text = formData.get('message');
addOptimistic(text);
await sendMessage(text);
}
return (
<>
{optimistic.map((m, i) => (
<div key={i}>{m.text}{m.sending && <small> sending…</small>}</div>
))}
<form action={action}>
<input name="message" />
</form>
</>
);
}
The important behaviour is automatic: when the action finishes or throws, React reverts to the real state. You do not write the rollback, which is exactly the code that was previously getting it wrong.
Worth being honest about the trade. Optimistic updates make an interface feel instant and they make failure more jarring, because the user saw success and then saw it withdrawn. They suit high-success-rate, low-stakes actions — sending a message, toggling a like. They suit a payment badly.
The use API, and reading context conditionally
use reads a resource during render — a promise or a context — and unlike every other hook, it can be called conditionally.
import { use, Suspense } from 'react';
function Comments({ commentsPromise }) {
// Suspends until resolved; the boundary above shows the fallback
const comments = use(commentsPromise);
return comments.map(c => <p key={c.id}>{c.text}</p>);
}
function Page({ commentsPromise }) {
return (
<Suspense fallback={<p>Loading comments…</p>}>
<Comments commentsPromise={commentsPromise} />
</Suspense>
);
}
The conditional part matters more than it first appears, because it removes a whole category of awkward component splitting:
function Heading({ children }) {
if (children == null) return null;
// Legal with use, illegal with useContext
const theme = use(ThemeContext);
return <h1 style={{ color: theme.color }}>{children}</h1>;
}
One important constraint: do not create the promise inside the component that consumes it. A promise created during render is a new promise on every render, which produces an infinite suspend loop. Create it in a parent, in a framework loader, or in a cached function, and pass it down.
Context can now be rendered directly as a provider — <ThemeContext value={theme}> rather than <ThemeContext.Provider value={theme}> — which is a small cleanup that shows up in every application.
The quieter changes that remove real friction
refas a prop. Function components receivereflike any other prop.forwardRefis no longer needed for new code, which removes a wrapper from a great many component libraries.- Cleanup functions from ref callbacks. A ref callback can return a cleanup function, so setting up and tearing down an observer or a third-party widget lives in one place.
- Document metadata hoisting. Rendering
<title>,<meta>or<link>anywhere in the tree hoists it into the head automatically. For applications that were using a helmet library purely for this, that dependency can go. - Stylesheet and script deduplication. Rendering the same stylesheet or async script from several components loads it once, with precedence honoured.
- Better hydration error messages. Previously you got two vague warnings; now you get a diff showing what the server rendered against what the client expected. This alone saves hours on server-rendered applications.
useDeferredValuetakes an initial value, so the first render can show something cheap before the expensive value arrives.
The metadata and stylesheet changes are the sort of thing that sounds minor and quietly deletes a dependency and a category of ordering bug.
Upgrading, and what to expect
The migration is not dramatic, but it is not nothing:
- Run the codemods. The official upgrade guide ships them and they handle most mechanical changes.
propTypesanddefaultPropsare gone for function components. Use TypeScript, or default parameter values.- String refs are removed. They were deprecated for years; anything still using them needs converting to callback refs.
ReactDOM.renderandReactDOM.hydrateare removed in favour ofcreateRootandhydrateRoot.- Errors are reported differently. Uncaught errors go through
onUncaughtErrorandonCaughtErroron the root rather than being re-thrown, which changes how your error reporting hooks in.
Check your dependencies before starting. Libraries that reach into React internals — some animation and testing libraries historically did — are where upgrade pain concentrates, not in your own components.
One thing to be clear about: React Server Components and the server-side half of Actions require a framework that implements them. If you are on a client-side build, you get Actions, useActionState, useOptimistic, use, ref as a prop and the metadata handling, and you do not get server functions. That is still most of the value.
How this fits the rest of the stack
The consistent direction here is that state which was always derived from an async operation — pending, error, optimistic, resolved — now belongs to the framework instead of to your components. Less code, fewer places to get the rollback wrong.
What none of it changes is that those actions call a server, and the server has to be up, fast, and observable when something fails. On RunxBuild, that backend deploys from your GitHub repository as a service in Node, Next.js, Python, Go, Ruby, Java, .NET or Docker, with build and runtime logs in the same place so a failed action is a log line rather than a guess, environment variables per service, and rollback to the previous deploy when a release goes wrong. Managed MySQL and Postgres sit alongside on private networking, and a static frontend can ship from the same repository with 120GB of bandwidth included. To see what the frontend, the service and the database come to, the RunxBuild hosting calculator lists them as separate line items.
Useful related references:
- Best Serverless Platform for FastAPI in 2024 and 2025: What Actually Matters
- AWS vs GCP for Startups: Features, Pricing, and the Right Pick
- Does PostgreSQL COMMIT Release Memory? What Actually Gets Freed
- Services on RunxBuild
FAQ
What is an Action in React 19?
A function that uses an async transition. React manages the pending state, errors and form reset around it, so the four pieces of state most components hand-wrote for a submission become framework behaviour. useActionState wraps the pattern, and passing a function to a form’s action prop wires it up.
What does useOptimistic do?
It shows an expected result immediately while the real request is in flight, and reverts automatically if the action fails or completes differently. The automatic rollback is the valuable part, since that is the code most hand-rolled optimistic updates got wrong. Use it for low-stakes, high-success actions rather than for payments.
How is use different from useContext?
use can be called conditionally and inside loops, which no other hook allows, and it reads promises as well as contexts. That removes a lot of awkward component splitting. The one constraint is not to create the promise inside the component that consumes it, since a new promise each render causes an infinite suspend loop.
Do I still need forwardRef?
Not for new code. Function components now receive ref as an ordinary prop, so the wrapper is unnecessary. Existing forwardRef code continues to work, and the official codemods handle the conversion if you want to remove it.
Can I use React 19 features without a framework?
Mostly. Actions, useActionState, useOptimistic, use, ref as a prop and the document metadata hoisting all work in a client-side build. Server Components and server functions require a framework that implements them, so a plain Vite application gets most of the release but not that half.