React 18 to 19 Migration Guide: What Changes and Why Performance Improves

Navneet Bhayani brings real-world insights from the frontlines of web and software development. With expertise in PHP, WordPress, React, NodeJS, and web technologies, his goal is to simplify technology and bring industry knowledge to support digital growth.

React 18 to 19 Migration Guide

Quick Summary: React 19 adds Actions, plus the useOptimistic and useActionState hooks, so async forms and data updates need far less manual state code. New resource preloading APIs (preload, preinit, preconnect, prefetchDNS) let React hint the browser about critical assets earlier in the page load. Hydration is more forgiving of scripts and extensions that inject extra markup into real-world pages, which used to trigger unnecessary re-renders. Migration steps: upgrade to React 18.3 first, enable the modern JSX transform, run the official codemods, then test with a focus on Next.js, Redux, and any UI library with strict peer dependency rules. The technical upgrade usually takes a few days. Adopting the new APIs across a codebase is a separate, optional effort that can be spread over weeks.

Upgrading a production React app is rarely as simple as bumping a version number. React 19 changes how forms, transitions, and hydration behave under the hood, and those changes translate into real performance gains, but only if the migration is done properly.

Here’s what actually changed in React 19, how to plan the upgrade, and where teams tend to get stuck along the way.

React 19 shipped in December 2024, with 19.1 and 19.2 following through 2025 to round out the release. It’s not a rewrite of React. Think of it as a second pass on the concurrent rendering model React 18 introduced in 2022, aimed at three things: less boilerplate for async UI, faster perceived load times, and steadier hydration on pages that carry third-party scripts.

If your app leans on forms, live data, or a lot of client-side fetching, React 19 will likely trim your code and smooth out some jank. If your app is mostly static content, the payoff is smaller, and the upgrade becomes more about staying current than chasing speed.

This matters most for teams doing serious react js development work, where forms, dashboards, and interactive flows are the bulk of the codebase.

Actions and async transitions. 

Functions passed to startTransition are now called Actions, and a transition can bundle one or more of them together in a single commit, according to the React 19 changelog on GitHub. In plain terms, that replaces hand-rolled isPending and error state with a pattern React manages for you, so the UI stays responsive without extra plumbing.

useOptimistic for instant feedback. 

This hook lets the interface update before the real response comes back, as explained in the React documentation for useOptimistic. A like button, a comment box, or a name change can look updated right away, while React reconciles the real result behind the scenes and rolls back automatically if the request fails.

useActionState for form state. 

Instead of four separate useState calls for data, loading, error, and success, this hook wraps an action and returns its result and pending status together, per the official React 19 release notes. It’s a smaller API surface for a very common pattern.

Resource preloading APIs. 

React 19 ships preinit, preload, prefetchDNS, and preconnect, which move resource discovery out of stylesheet loading and let React hint the browser earlier, as noted in the same changelog. This gives you a React-native way to prioritize fonts, scripts, and critical assets instead of hand-writing <link> tags.

More resilient hydration. 

Mismatches between server and client output used to trigger vague warnings and, in some cases, a full client-side re-render. React 19 reports hydration errors with the actual mismatch instead of a generic message, which cuts down on debugging time when a browser extension or ad script has quietly added extra DOM nodes.

The use() API. 

This new function reads a promise or context value directly during render and suspends until it resolves, per the React 19 upgrade guide. It pairs naturally with Suspense for data fetching.

ref as a prop. 

Function components can now receive ref like any other prop, so forwardRef is no longer required for new components. It’s a small change, but it removes a common source of extra wrapper components.

FeatureReact 18React 19Migration EffortPerf Impact
Async state handlingManual useState for pending, error, and dataActions inside useTransition handle pending and error automaticallyMediumSmoother UI during async work
Optimistic UICustom logic to show and revert temporary stateuseOptimistic hook built inLowFaster perceived response
Form stateMultiple useState calls per formuseActionState combines result, pending, and errorLow to MediumLess re-render overhead
Resource hintsManual <link rel=”preload”> tagspreload, preinit, preconnect, prefetchDNS from react-domMediumFaster initial resource loading
Ref handlingRequires forwardRef for function componentsref passed as a normal propLowSimpler render tree, fewer wrappers
Hydration mismatchesOften triggers a full client re-renderMore resilient, with clearer error messagesLow (automatic)Fewer unnecessary re-renders

A step-by-step path from React 18 to React 19, written so you don’t need to cross-reference any other article to execute it correctly.

1. Upgrade to React 18.3 first

React 18.3 behaves identically to 18.2 in production, but it adds deprecation warnings for every API that will actually break under React 19. This turns a risky one-shot jump into two safe, verifiable steps: first you find every breakage while still on a stable, working version, then you upgrade the major version with confidence that the codebase is already compatible.

  • Install it: npm install react@18.3 react-dom@18.3
  • Run your app and your test suite, then watch the console for deprecation warnings. They name the exact API and file.
  • Fix each warning before moving on. Common ones: propTypes on function components, string refs, and legacy context usage.
  • The official React 19 upgrade guide documents the full warning list if you want to cross-check coverage.

2. Turn on the modern JSX transform

React 19 relies on the new JSX transform (the one that doesn’t require import React from ‘react’ in every file) to support newer optimizations, including passing ref as a regular prop instead of requiring forwardRef.

  • Babel: confirm @babel/preset-react is on version 7.9+ with runtime: ‘automatic’ set in your Babel config.
  • TypeScript: confirm tsconfig.json has “jsx”: “react-jsx” (not “react”).
  • Create React App / Next.js / Vite: these have shipped with the automatic transform by default for years, so most projects need no change, but verify if your build config hasn’t been touched recently or was hand-rolled.
  • If you find the classic transform still active, switching it is safe and self-contained; it doesn’t require any code changes beyond removing now-unnecessary import React statements (optional, not required).

3. Install React 19

npm install react@^19.0.0 react-dom@^19.0.0

Then update your type definitions if you’re on TypeScript:

npm install –save-dev @types/react@^19.0.0 @types/react-dom@^19.0.0

At this point your app will likely not build cleanly yet. That’s expected. The next step handles the mechanical fixes.

4. Run the official codemods

React ships codemods specifically so you don’t have to hand-edit every file. Run them from your project root:

npx codemod@latest react/19/migration-recipe

This recipe bundles several individual transforms, each targeting one breaking change:

  • Removes propTypes and defaultProps from function components: these were deprecated in favor of default parameter values and TypeScript/runtime validation, and React 19 no longer reads them from function components at all.
  • Replaces legacy context (contextTypes, getChildContext) with the modern createContext API, since the legacy context system is removed entirely in React 19.
  • Adds an explicit initial argument to useRef calls: useRef() with no argument is no longer valid; it now requires useRef(null) or an explicit initial value, so the codemod inserts null wherever one is missing.

Run your test suite immediately after. Codemods handle syntax faithfully but won’t catch every behavioral edge case, particularly around ref-heavy component libraries.

5. Adopt new APIs gradually

You don’t need to rewrite every form or data-fetching flow on day one. React 19’s biggest additions (useActionState, useOptimistic, the use() hook, and native form Actions) are opt-in improvements, not requirements. The upgrade works with your existing patterns.

Prioritize by user-facing impact rather than code age:

  • Checkout and sign-up flows benefit most from useOptimistic, since users are most sensitive to lag between clicking “Submit” and seeing a response.
  • Forms hitting slow backends benefit from useActionState, which gives you pending state and error handling without manually wiring useState + try/catch boilerplate.
  • Leave stable, rarely-touched internal tooling as-is until you have a concrete reason to change it. Migrating for its own sake adds review risk without user-facing benefit.

6. Add resource hints where they matter

React 19 adds built-in support for resource hints (preload, preconnect, prefetchDNS, preinit) that you can call directly from components, rather than manually managing <link> tags in your document head.

  • preconnect: use for third-party domains you’ll definitely call soon (analytics, payment providers, font CDNs), since it establishes the DNS + TLS handshake early, before the actual request is made.
  • preload: use for specific resources you know you need (a critical font file or a script) so the browser fetches it in parallel with rendering instead of waiting to discover it.
  • Apply these early in the component tree (e.g., in a layout or root component) so hints fire on initial page load rather than after the resource is already needed.
  • Don’t over-apply this. Preloading everything defeats the purpose by competing for the same early-load bandwidth. Reserve it for resources on the critical rendering path.

7. Test thoroughly, especially server-side rendering

React 19 changes hydration internals, so SSR apps need closer scrutiny than client-only ones.

  • Hydration mismatches: check the console for hydration warnings in dev mode. React 19 is stricter about surfacing these than 18 was, so previously-silent mismatches (e.g. date formatting differences between server and client) may now be visible for the first time.
  • Error boundaries: confirm they still catch errors from the components you expect. React 19 changes some internal error-reporting behavior (errors are now also reported to onCaughtError/onUncaughtError root options if you’ve set them), so verify your logging/monitoring integration still fires correctly.
  • Suspense boundaries: if you use Suspense for data fetching or code-splitting, test loading and fallback states explicitly, since timing behavior around Suspense has been refined in React 19.
  • Run this pass in a staging environment that mirrors production SSR configuration, not just local dev, since hydration bugs are often environment-specific.

8. Benchmark before and after

Don’t treat the migration as done once it builds and passes tests. A “successful” upgrade can still be a performance regression if you don’t measure it. React 19’s performance gains are real but not automatic; they come from features you have to use (like the compiler-friendly patterns and reduced re-render behavior), so a like-for-like benchmark is the only way to know whether your app actually got faster.

Capture a baseline before you touch anything, ideally right after step 1 (React 18.3, before any React 19 code):

  • Core Web Vitals: LCP (Largest Contentful Paint), INP (Interaction to Next Paint), and CLS (Cumulative Layout Shift). Use Lighthouse or Chrome’s PageSpeed Insights for lab data, and the Chrome UX Report or your own RUM (real user monitoring) tool for field data. Lab and field numbers can diverge, so capture both if you can.
  • React-specific render metrics: use the React DevTools Profiler to record commit counts and render duration for your highest-traffic components (typically your main list views, checkout flow, or dashboard). Save the profile as a baseline file, since the Profiler lets you export and later diff against a new recording.
  • Bundle size: run your existing bundle analyzer (webpack-bundle-analyzer, vite-bundle-visualizer, or similar) and note the total and per-chunk sizes. React 19’s package size differs from 18’s, and this changes with your bundler’s tree-shaking behavior.
  • Server response and hydration timing: for SSR apps, log time-to-first-byte and time-to-hydration-complete. React 19 changes hydration internals (step 7), so this is where regressions or improvements are most likely to show up.

A useful Tip: Re-measure after the full migration, using the same pages, network conditions, and device profile (throttle to a consistent CPU/network setting in Chrome DevTools, or use the same physical test device, so you’re not comparing a fast machine against a slow one).

  • defaultProps on function components is gone. Use default parameter values instead.
  • Legacy context (contextTypes, getChildContext) is removed. Move to the Context API or Hooks.
  • useRef() now needs an argument. Calling it with nothing will raise a TypeScript error, so pass undefined explicitly where that’s the intent.
  • useFormState is deprecated in favor of useActionState. Next.js’s own upgrade documentation confirms useFormState still works in React 19 but is on its way out, with useActionState as the recommended replacement.
  • Callback refs shouldn’t return a value. If your callback ref has an implicit return (an arrow function without braces), wrap it in braces so it returns nothing.

Libraries that reach into React internals. 

A handful of older state management libraries relied on private React APIs rather than the public ones. Recoil is the well-known example, and it’s now archived and unmaintained, so it doesn’t work cleanly under React 19. If your app depends on something like this, plan a move to an actively maintained option such as Redux Toolkit, Zustand, or plain Context.

Next.js version alignment. 

This one catches teams off guard. Next.js 15’s initial release only worked with the React 19 release candidate. Full stable support arrived with Next.js 15.1, which made React 19 official across both the Pages Router and the App Router. If you’re on an older Next.js version, upgrade it alongside React rather than trying to do one without the other. The Next.js version 15 upgrade guide walks through the process, including the codemod command.

Peer dependency warnings from UI libraries. 

Component libraries that pin exact React versions will throw install errors until their maintainers update them. Check each library’s changelog for React 19 support before reaching for –legacy-peer-deps, since that flag hides genuine incompatibilities along with the harmless warnings.

TypeScript type changes. 

Some types got stricter, and a few defaults changed. Run the TypeScript codemod for React 19 and expect to touch a handful of files, mostly around refs and event types.

SSR and hydration debugging. 

When server and client output diverge, usually because of dates, random IDs, or browser-only APIs, React 19’s hydration error messages point to the exact mismatch instead of a vague warning. That alone saves real debugging time on larger apps.

Compare render counts and commit times in React DevTools’ Profiler before and after the upgrade. Don’t assume the numbers improved just because the version did.

Pair preload and preconnect calls with actual Core Web Vitals tracking, using Lighthouse or the web-vitals library, so you’re confirming real gains rather than theoretical ones.

Roll out useOptimistic first on the actions users notice most, like posting a comment or saving a setting, where instant feedback has the biggest visible payoff.

Migrate high-traffic forms and fetch calls first. Leave the rest for later once you’ve confirmed the pattern works well for your app.

Revisit any manual debouncing or batching workarounds built for React 18. Some of that code may no longer be necessary.

Good time to upgrade if:

  • You’re starting a new project
  • You’re already on Next.js 15.1 or later
  • Your team has room to run a full regression test pass

Better to wait if:

A library you depend on hasn’t published React 19 compatibility yet

Your SSR setup is custom and complex

Your team doesn’t have bandwidth for thorough testing right now

React 19 is largely backward compatible, but “largely” isn’t the same as “fully.” The libraries you depend on matter as much as React itself when it comes to a smooth migration.

If your team is weighing whether to handle this in-house or bring in extra hands, it can help to talk to developers who migrate React codebases regularly. You can hire ReactJS developers for the upgrade itself or for ongoing react development after it ships. 

React 19 refines how async state, form handling, and resource loading work rather than reinventing React. The migration itself is mostly mechanical: upgrade to 18.3, run the codemods, check your dependencies, test. The real work is deciding where Actions, useOptimistic, and preloading will actually move the needle on your app’s performance, not just its version number.

Planning a react migration and want a second opinion on scope or timeline? Get in touch with team Pennine Technolabs, and we’ll walk through what it looks like for your specific stack.

Share On:

Let’s Discuss Your Project Idea.

    Protect
    Upload document

    Drag And Drop Or Browse Your File (Max upload size : 10MB)

    Subscribe to Our Newsletter

    Join the Pennine Family! The best way to stay updated with Web technologies and be informed of the latest Pennine Technolabs blogs.

    * indicates required