Confession: when React 19 went stable, I rewrote half of my dashboard the same weekend. Then I rewrote about a third of it back, because most of what I changed didn’t earn its keep. Six months in, I’ve got opinions.
This isn’t a “what’s new” rundown. The official React 19 release notes already exist and they’re fine. This post is about what survived contact with a real codebase. The parts I keep reaching for. A few I forget exist. And the ones I quietly reverted.
Actions and useActionState replaced my form reducers
The biggest day-to-day change for me is useActionState plus form Actions. I had a small mountain of useReducer({ status, error, fieldErrors }) plumbing for forms. Most of it is gone now.
Before, every form needed roughly this:
function ContactForm() {
const [state, dispatch] = useReducer(formReducer, initialState);
const [pending, setPending] = useState(false);
async function handleSubmit(e) {
e.preventDefault();
setPending(true);
try {
await submitContact(new FormData(e.target));
dispatch({ type: 'success' });
} catch (err) {
dispatch({ type: 'error', error: err.message });
} finally {
setPending(false);
}
}
// ...
}
After:
function ContactForm() {
const [state, formAction, pending] = useActionState(submitContact, { ok: false });
return (
<form action={formAction}>
{/* fields */}
<button disabled={pending}>Send</button>
{state.error && <p role="alert">{state.error}</p>}
</form>
);
}
There’s no clever trick here. The hook just bundles the three things every form needs (last result, action wrapper, pending bit) into one return value. I find I’m thinking about forms less now, which is the highest compliment a hook can earn from me.
The catch: it’s <form action={...}>, not onSubmit. That trips me up about once a week when I’m copy-pasting between files. The compiler doesn’t warn you that an onSubmit handler is doing nothing useful next to an action prop.
The React Compiler is the one I almost missed
Honestly? I almost skipped the React Compiler at first. Memoization is one of those things I assumed I was already handling correctly, and I figured the speedup wouldn’t be worth a build-tool change.
I was wrong about both. The compiler caught more rerenders than I expected, including a couple of context providers that were re-creating object literals on every render and triggering whole subtrees to update. I wrote about what the React Compiler actually does to your code once I’d lived with it for a month, but the short version: it’s the closest thing React has shipped to “free performance in exchange for a config flag,” and you should at least try it on a branch.
Two warnings. First, it’s strict about the rules of React. If your components have side effects in render (mine had two), the compiler will refuse to optimize them and tell you why. Fix the side effect; don’t disable the rule. Second, it doesn’t replace profiling. It catches the boring cases. The interesting ones, like a chart re-rendering because a parent updated now, still need eyes on the flame graph. The official React Compiler reference covers the install path. The linting plugin is the bit I’d install first.
useOptimistic only earns its keep on lists, not buttons
I had high hopes for useOptimistic. In practice I use it for one thing: list updates where a server round-trip would feel laggy. Adding a comment, toggling a like, reordering items in a board.
For single-button actions like submit, save, or delete, useActionState’s pending flag is enough on its own. Wrapping a button in optimistic state just to flicker a label feels like reaching for a cannon to crack a walnut.
If you want a worked example of where it actually pays off, I wrote one up in the useOptimistic post I keep referring myself back to. Short version: think of it as “render this row immediately, then reconcile when the server replies,” not “add fake loading states everywhere.”
Server actions and server components are two different mental models
This one’s caused me more confusion than anything else in React 19, and I see it in code reviews from colleagues too.
Server components are about where the JSX runs. They render on the server, never ship JS to the client, and are great for fetch-heavy pages where the data shape is known at request time.
Server actions are about where mutations run. They’re functions you mark with 'use server' and call from a form or a button. The transport story is RSC-flavored, but you can use server actions in a perfectly ordinary client component. I cover the day-to-day shape of this in my post on dropping API routes for server actions.
The reason this trips people up: both ship under the same React 19 banner, both touch the network, and both have the word “server” in them. They are not the same feature. If you find yourself asking “do I need a server component for this?”, the answer is almost always “no, you need a server action.” The official server functions reference is worth ten minutes if you’re still fuzzy.
ref as a prop killed forwardRef in my codebase
Small win, but a real one. In React 19 you can pass ref as a normal prop, and forwardRef is on its way out. My Button and Combobox components both lost a wrapper.
Before:
const Button = forwardRef(function Button({ children, ...rest }, ref) {
return <button ref={ref} {...rest}>{children}</button>;
});
After:
function Button({ ref, children, ...rest }) {
return <button ref={ref} {...rest}>{children}</button>;
}
Same behavior, less ceremony. The codemod that ships with React handled most of my codebase in one pass. The one place it didn’t was a generic forwardRef<HTMLDivElement, Props> that needed me to type the prop manually, but that took thirty seconds.
What I tried and reverted
Two features I rewrote toward and then rewrote away from.
First, use(promise) inside client components, with Suspense boundaries everywhere. I liked the idea of data-fetching that reads as if it were synchronous. In practice it pushed me into Suspense fallback hell, where every minor loading state needed its own wrapper. For pages with one or two fetches, the older useEffect + useState (or a real data library) felt cleaner. I’ll come back to use when I have a clearer mental model for boundary placement.
Second, the new document metadata APIs that let you drop <title>, <meta>, and <link> directly inside components. They work, but my Next.js app already has generateMetadata doing the same job at the route level, and mixing the two felt fragile. If you’re on a non-Next stack the new APIs are great. On Next, I’d stick with what’s already there.
What to actually try this week
If you’re still on React 18, here’s the thing I’d do first: make a branch, install the React Compiler eslint plugin, and let it scream at you for an hour. You’ll learn more about your codebase from those warnings than from any blog post, including this one.
I keep notes like these in my work log over on abrarqasim.com so I don’t have to re-learn the same lesson every time React ships a feature. If a particular React 19 corner is still confusing you, ping me. I’m always looking for the next thing to write up.