HTML / CSS

React 19 in Production: The Features That Actually Changed How You Write React

React 19 is the release where React stops asking you to do the framework's job. The theme is clear: less boilerplate, more automation.

29 Apr 2024

React 19 in Production: The Features That Actually Changed How You Write React

React 19 shipped as stable in December 2024. The framing that stuck at the time was "less boilerplate," but that undersells it. The real theme is that React finally does a set of jobs it used to hand to you: memoization, form state, metadata, ref plumbing. Here is what actually landed, and how it changes the code you write.

First: the React Compiler is not part of React 19

This is the most common misconception, so clear it up before anything else. The React Compiler shipped as a separate product. It entered public beta alongside React 19 at the end of 2024 and reached 1.0 in October 2025. React 19 the library and the compiler are two different releases on two different timelines.

What the compiler does is real, though. For years you wrote useMemo, useCallback, and memo by hand, maintaining dependency arrays and debugging stale closures when you got them wrong. The compiler analyzes your components at build time and inserts that memoization for you.

Tsx
// You wrote this by hand for years:
const value = useMemo(() => expensive(a, b), [a, b]);
const onClick = useCallback(() => handle(a), [a]);

// The compiler infers the same memoization at build time,
// so you write the plain version and it stays fast.
const value = expensive(a, b);
const onClick = () => handle(a);

By 2026 the compiler is the default assumption in new projects. Next.js, Expo, TanStack Start, and the Vite-based frameworks each wire it into their build pipeline, and the story is "on by default, turn it off if you hit an edge case." Adopt it, but adopt it as its own decision, not as a React 19 feature.

Actions: async transitions become first-class

Before 19, every mutation meant the same manual dance: set a pending flag, try/catch the request, set an error, reset pending. React 19 builds this into transitions. An async function passed to a transition or a form action is an Action, and React tracks pending and error state for you.

Tsx
import { useActionState } from "react";

async function updateName(_prev: string | null, formData: FormData) {
  const error = await saveName(formData.get("name") as string);
  return error ?? null;
}

function NameForm() {
  const [error, submitAction, isPending] = useActionState(updateName, null);
  return (
    <form action={submitAction}>
      <input name="name" />
      <button disabled={isPending}>Update</button>
      {error && <p role="alert">{error}</p>}
    </form>
  );
}

useActionState (renamed from the beta's useFormState) returns the last result and a pending flag. No useState for loading, no manual reset.

useFormStatus for nested submit buttons

A child deep inside a form can read the parent form's pending state without prop drilling:

Tsx
import { useFormStatus } from "react-dom";

function SubmitButton() {
  const { pending } = useFormStatus();
  return <button disabled={pending}>Save</button>;
}

useOptimistic for instant UI

Show the result before the server confirms, and let React roll back automatically if the Action fails:

Tsx
const [optimisticMessages, addOptimistic] = useOptimistic(
  messages,
  (state, next: string) => [...state, { text: next, sending: true }],
);

The use() API

use reads the value of a promise or context. Unlike every other hook, you can call it conditionally, inside an if or a loop. Reading a promise suspends the component until it resolves, which pairs cleanly with Suspense.

Tsx
import { use } from "react";

function Comments({ commentsPromise }: { commentsPromise: Promise<Comment[]> }) {
  const comments = use(commentsPromise); // suspends until resolved
  return comments.map((c) => <p key={c.id}>{c.text}</p>);
}

ref is just a prop now

forwardRef is obsolete. Function components accept ref as a normal prop, so the wrapper that wrapped half your design system disappears.

Tsx
function Input({ ref, ...props }: React.ComponentProps<"input">) {
  return <input ref={ref} {...props} />;
}

Ref callbacks can also return a cleanup function, which finally gives you a symmetric setup and teardown instead of the awkward null call on unmount:

Tsx
<input
  ref={(node) => {
    node?.focus();
    return () => {
      // teardown when the element unmounts
    };
  }}
/>

Document metadata, stylesheets, and scripts

Render <title>, <meta>, and <link> anywhere in the tree and React hoists them into <head>. For a lot of apps this removes the need for a helmet library entirely, and it works with client rendering, streaming SSR, and Server Components.

Tsx
function BlogPost({ post }: { post: Post }) {
  return (
    <article>
      <title>{post.title}</title>
      <meta name="description" content={post.excerpt} />
      <link rel="canonical" href={post.url} />
      <h1>{post.title}</h1>
    </article>
  );
}

React 19 also understands stylesheet precedence and async scripts, deduplicating them and ordering them correctly across components.

Server Components and Server Actions are stable

The RSC APIs that spent years behind the "experimental" label are stable in React 19. Server Components render on the server and ship no JavaScript for their own logic. Server Actions let a Client Component call a server function directly, wired into the same action prop as the form features above. If you are on a framework that supports them (Next.js, and increasingly others), they are now a supported foundation, not a preview.

Should you upgrade?

The migration is mostly mechanical, with a few sharp edges:

  • forwardRef still works but is deprecated. A codemod rewrites most usages.
  • Removed APIs: legacy string refs, propTypes and defaultProps on function components, and the legacy Context API are gone. If you have old code, this is the real work.
  • Stricter types: the @types/react 19 update tightens ref and ReactNode, which surfaces latent type errors on upgrade.

Run the upgrade on a branch, let the type-checker find the breakage, and treat the compiler as a separate follow-up once you are on 19.

Wrapping up

React 19 is not a grab bag of small features. It is React taking over the busywork you used to hand-write: memoization (via the separately shipped compiler), form and mutation state (Actions), and document metadata. Write the plain version, let the framework keep it correct and fast. That is the shift, and it is stable and in production now, not a sneak peek.

Go further
Live cohort on Maven

From Senior to Staff: Master the Architecture Skills That Get You Promoted

Go from shaky in design reviews to the engineer everyone trusts to architect the hard stuff.

View the live cohort

Keep reading