Skip to content

React 19: New Features and Migration Strategy

What in React 19 is worth adopting mid-2025 versus what you can ignore - migration without rewrite religion.

5 min read
React 19
Migration
Actions
use
Frontend
TypeScript

React 19 shipped in December 2024. By mid-2025 most teams are past asking whether it is real. The work is what to adopt now versus what to leave alone until a framework or a painful edge forces the move.

I do not treat major React versions as a loyalty test. I treat them as a migration surface with failure modes.

What actually changed (the short list)

For an app engineer in June 2025, the headline features that change day-to-day code are:

  1. Actions: async functions wired to forms / transitions, with pending and error handling that React owns more of.
  2. use: read a promise or context during render (with Suspense), instead of inventing yet another data hook shape.
  3. ref as a prop: function components can take ref without forwardRef ceremony in many cases.
  4. Document metadata and stylesheets as components: <title>, <meta>, <link> in the tree; useful when your framework already leans that way.
  5. Improved hydration / error reporting: quieter wins that show up when something breaks, not in demos.

Server Components are stable in the Next App Router world. They are not a requirement to upgrade React on a Vite SPA. Confusing those two decisions is how teams schedule a rewrite they did not need.

Adopt now vs ignore (decision table)

SurfaceAdopt mid-2025?Why
React 19 on an existing client appYes, when deps allowUnlocks actions/use/ref-as-prop; staying on 18 is fine until you need them
Actions for forms + mutationsYes, where you own the form UXCuts boilerplate around pending/error; still needs a server or mutation layer
use(promise) as your data layerNo as a defaultGreat with Suspense boundaries; a poor replacement for a cache policy (see TanStack Query later this month)
Drop all forwardRef overnightOpportunisticMigrate on touch; big-bang ref PRs are noise
Rewrite SPA → RSC because "React 19"NoRSC is a framework architecture choice, not a React version checkbox
New compiler / automatic memo dreamsWait for your toolchainDo not bet the quarter on blog-post performance mythology

Migration strategy without rewrite religion

I stage upgrades like dependency surgery:

  1. Inventory blockers: peer deps, testing libraries, UI kits still pinned to React 18 types.
  2. Upgrade React + types in one PR with the smallest app that still boots tests and Storybook.
  3. Fix breaking changes (removed APIs, stricter types) before adopting new APIs.
  4. Adopt one new pattern in a leaf feature: usually an action-backed form or a ref cleanup, and write the team rule from that PR.
  5. Expand on touch: no "React 19 rewrite" epic.
tsx
// features/invite/InviteForm.tsx, illustrative action-shaped mutation 'use client'; import { useActionState } from 'react'; type State = { ok: true } | { ok: false; message: string } | null; async function sendInvite(_prev: State, formData: FormData): Promise<State> { const email = String(formData.get('email') ?? ''); if (!email.includes('@')) { return { ok: false, message: 'Enter a valid email.' }; } // call your API / server action here await fetch('/api/invites', { method: 'POST', body: JSON.stringify({ email }), headers: { 'Content-Type': 'application/json' }, }); return { ok: true }; } export function InviteForm() { const [state, action, pending] = useActionState(sendInvite, null); return ( <form action={action}> <label> Email <input name="email" type="email" required disabled={pending} /> </label> <button type="submit" disabled={pending} > {pending ? 'Sending…' : 'Send invite'} </button> {state && !state.ok ? <p role="alert">{state.message}</p> : null} {state?.ok ? <p>Invite sent.</p> : null} </form> ); }

That snippet is the point of Actions for me: pending and error belong next to the form, not in a bespoke store you invent for every screen. It still assumes you own validation, auth, and idempotency on the server.

Where use pays off: and where it lies

tsx
// routes/invoice.tsx, illustrative; needs a Suspense boundary above import { use } from 'react'; function InvoiceDetails({ invoicePromise, }: { invoicePromise: Promise<Invoice>; }) { const invoice = use(invoicePromise); return <h1>{invoice.number}</h1>; }

use is excellent when the promise is already a first-class part of your router/loader story. It is a poor excuse to delete your cache layer. Deduping, stale-while-revalidate, retries, and shared invalidation still need a policy - React will not invent one because you called use.

Failure modes

Equating React 19 with "we must do RSC." Client apps can upgrade without changing their hosting model.

Big-bang adoption of every new API. You will ship subtle Suspense and form regressions with no rollback story.

Actions without server ownership. Pending UI over a fire-and-forget fetch that ignores 409s is cosplay.

Ignoring peer dependency lag. The upgrade PR is not done until test utilities and the design system compile.

Performance narratives without measurement. New APIs are not a substitute for profiling the actual slow path.

Continuity

React 19 is available and boring in the best way; adopt on purpose. File-based routing conventions across the frameworks we already mapped in April keep navigation a decision instead of a tribe.

Takeaway

Upgrade React 19 when dependencies allow; adopt Actions and use where they delete real boilerplate. Do not confuse a library version with an architecture rewrite: RSC is a product bet, not a checkbox on the React release notes.