react / react-19 / frontend
React 19 Actions, useOptimistic, and the ref change nobody mentions
The client-side parts of React 19 we actually use every day, plus the one TypeScript change to refs that broke our build the first afternoon.
- Published
- May 31, 2026
- Read
- 4 min
Contents
React 19 has been stable for over a year now, and the line is up to 19.2. Most of the attention went to Server Components, but the changes that actually shaped how we write client code every day are smaller and quieter: Actions, a few new hooks, and one TypeScript change to refs that broke our build the first afternoon. Here is what we lean on, and what trips people up.
Actions replace the loading-state boilerplate
An Action is just an async function you hand to a form or a transition. React manages the pending state, surfaces thrown errors to the nearest error boundary, and resets the form on success. You stop writing the same isLoading and try/catch dance on every submit.
The hook you reach for is useActionState. It lives in react now, not react-dom, and it replaces the old useFormState:
const [error, submit, isPending] = useActionState(
async (prev, formData) => {
const res = await saveProfile(formData)
if (!res.ok) return res.message
return null
},
null,
)
return (
<form action={submit}>
<input name="email" />
<button disabled={isPending}>Save</button>
{error && <p>{error}</p>}
</form>
)For design-system submit buttons that should not know about their parent form, useFormStatus reads the nearest form's pending state without prop drilling. One Spinner component, dropped anywhere inside a form, that just works.
useOptimistic, and the rule people miss
useOptimistic lets you show a provisional value while an Action runs, then reverts cleanly if it fails. It is great for things like a comment that appears instantly while the request is still in flight.
The rule people miss: optimistic state only holds while a transition or Action is pending. If you call the setter outside that context, the value snaps right back and you are left confused. It is not a general-purpose state setter, it is a view over a pending transition. Treat it that way and it behaves.
The ref change that broke our build
Two ref things changed in 19. First, function components now receive ref as an ordinary prop, so forwardRef is deprecated and there is a codemod to strip it. That part is pleasant.
Second, ref callbacks can now return a cleanup function that runs on unmount. Useful, but it has a side effect that cost us twenty minutes of squinting at a red build: TypeScript now rejects an implicit return from a ref callback, because that return value is interpreted as the cleanup function. This stops compiling:
// Error: implicit return is read as a cleanup function
<div ref={el => (nodeRef.current = el)} />
// Fix: use a block body so nothing is returned
<div ref={el => { nodeRef.current = el }} />The arrow-with-parens assignment pattern was muscle memory for a lot of us. Now it is a type error. The fix is trivial once you know the cause, but the error message does not point at the cleanup feature, so it reads as nonsense until it clicks.
One thing to keep straight
People conflate "React 19" with "Server Components and Server Actions." Those are framework features, delivered by something like the Next.js App Router. A plain Vite single-page app running React 19 still has no Server Components. What it does get are the client pieces: use, which can read a promise or context and may be called conditionally, plus useActionState and useOptimistic. Knowing which bucket a feature lives in saves a lot of confused debugging.
None of this is revolutionary, and that is the point. React 19 mostly deletes boilerplate you were already tired of writing. The Actions model is the part we would not want to give back.