React 19 shipped as stable in December 2024, and the churn since then has been less about new APIs and more about teams finally upgrading and discovering which of their patterns broke. If you run a React codebase of any real size, the current state of JavaScript React News is dominated by three things: the React Compiler moving past its experimental label, Server Components becoming the default mental model in Next.js and Remix, and a slow, quiet exodus from the useEffect-everywhere style of code that was normal three years ago. This guide walks through what has actually changed in practice, what breaks during the upgrade, and which patterns are worth rewriting now.
React 19 is stable, but the upgrade is not a no-op
The headline from the React 19 release post on react.dev is the new hooks — useActionState, useFormStatus, useOptimistic, and the use function for unwrapping promises and context inside render. The less-discussed part is the list of removed APIs and changed behaviors. If you are upgrading a codebase that has been alive since React 16, expect these to bite:
propTypesanddefaultPropson function components are gone.defaultPropsstill works on class components for now, but function components must use default parameter values. Grep your tree for.defaultProps; any match on a function component is a silent bug waiting to happen.- String refs were removed. If you still have
ref="myInput"anywhere, it will throw. - Legacy context (
contextTypes,getChildContext) is gone. Rare in new code, common in codebases with a long jQuery-to-React migration history. ReactDOM.renderandReactDOM.hydratewere removed in React 18, but 19 removed the compat shims some teams were still relying on.
The upgrade itself is documented in the React 19 upgrade guide, which is the page to actually read start-to-finish rather than skim. The codemods it ships via npx types-react-codemod handle the TypeScript ReactElement and ref prop changes, but they do not cover everything. In particular, the new rule that ref is now a regular prop — no more forwardRef — is a mechanical change your codemod will miss in libraries that wrap your components.
forwardRef is no longer needed
This is the single biggest ergonomic win in 19 and worth understanding before you touch anything else. In React 18, passing a ref through a component required forwardRef:
“`javascript
// React 18 style
const FancyInput = React.forwardRef(function FancyInput(props, ref) {
return ;
});
“`
In React 19, ref is just a prop. The equivalent is:
“`javascript
// React 19 style
function FancyInput({ ref, …props }) {
return ;
}
“`
You do not have to rewrite your existing forwardRef calls — they still work — but new components should drop it. The subtle trap: if you maintain a component library with a public API where consumers call myRef.current, the behavior is identical, but TypeScript types for the ref prop now need to be written manually instead of inferred from the forwardRef generic. Expect a round of type errors in any .d.ts file you publish.
The React Compiler is the actual story
Every major React release of the last five years has shipped features most apps did not need. The React Compiler is different. It rewrites your components at build time to add the memoization you would otherwise have to write by hand with useMemo, useCallback, and React.memo. The pitch is simple: stop thinking about memoization; write the obvious code; let the compiler do it.
The compiler moved to release candidate in late 2024 and is documented on the React Compiler docs page. Installation is a Babel plugin (or an SWC plugin for Next.js 15+):
“`bash
npm install –save-dev babel-plugin-react-compiler
“`
Then in babel.config.js:
“`javascript
module.exports = {
plugins: [
[‘babel-plugin-react-compiler’, {
target: ’19’, // or ’18’ or ’17’
}],
],
};
“`
There are two things to understand before you turn it on. First, it only compiles components and hooks that follow the Rules of React. If your code mutates props, reads refs during render, or has a hook that conditionally calls another hook, the compiler skips that component and logs a diagnostic. This is a feature, not a bug — it is the compiler telling you which of your components have latent bugs that only haven’t bitten yet because React’s current reconciler is forgiving.
Second, running the included ESLint plugin, eslint-plugin-react-compiler, is how you actually find those violations at lint time instead of at build time. Turn it on before you enable the compiler. On a codebase of any real age, you will discover dozens of places where a component writes to a ref during render or mutates a prop inside a callback. Fix those first.

What the compiler does not solve
A compiler that auto-memoizes does not make a slow app fast. It stops you from shipping a fast app that becomes slow because someone forgot a useCallback in a hot path. The actual performance work — reducing bundle size, avoiding unnecessary server round-trips, picking the right caching strategy — is still yours. If your app is slow today because it re-renders a 2,000-item list on every keystroke, the compiler will help. If it is slow because your API waterfall takes 900ms, the compiler will do nothing.
Server Components have quietly eaten the mental model
For about two years, Server Components were a thing that existed in Next.js App Router and almost nowhere else. That has changed. Remix (now merged into React Router 7) ships them, Waku is a reference implementation, and the react-server package in the React monorepo is the canonical source for anyone building a framework on top.
The practical shift is that the question “should this be a client component or a server component?” is now the first question you ask when you add a file, not something you figure out later. The rule in practice:
- If the component reads from a database, calls an internal API with secrets, or renders content that never needs interactivity — it’s a server component. No
'use client'directive. - If it uses
useState,useEffect, event handlers, or browser APIs — it’s a client component. First line:'use client'. - If it mostly reads data but has one small interactive bit, split it: the outer component stays on the server, the interactive bit becomes a small client component imported from a separate file.
The anti-pattern I see most often is the reflexive 'use client' at the top of every file because “that’s how we did it in Pages Router.” This ships your entire component tree as JavaScript to the browser and defeats the whole point. A useful exercise: run next build and look at the First Load JS numbers. If your marketing pages are above 200KB, you almost certainly have stray 'use client' directives high in the tree.
useEffect is being un-taught
The react.dev documentation rewrite, which is now a couple of years old, introduced a page called You Might Not Need an Effect, and it has become the most-linked document in code reviews for a reason. The core point: most useEffect calls in a typical codebase are doing something they shouldn’t. The canonical offenders:
- Syncing state to derived state.
useEffect(() => setFullName(first + ' ' + last), [first, last]). Just compute it during render:const fullName = first + ' ' + last. - Resetting state on prop change. Use the
keyprop to remount the component instead. - Fetching data. Use a real data-fetching layer — TanStack Query, SWR, or the framework’s loader — not a bare
useEffectthat leaks on unmount. - Running “on mount” code that is actually initialization. Use a module-level side effect or a lazy initializer.
React 19’s new hooks make the legitimate uses of effects smaller still. Form submission with optimistic UI used to be a dance of useState, useEffect, an abort controller, and three guard conditions. Now it is useActionState plus useOptimistic, and the framework handles the transitions:
“`javascript
import { useActionState, useOptimistic } from ‘react’;
import { updateName } from ‘./actions’;
function NameForm({ currentName }) {
const [optimisticName, setOptimisticName] = useOptimistic(currentName);
const [state, formAction, isPending] = useActionState(
async (_prev, formData) => {
const next = formData.get(‘name’);
setOptimisticName(next);
return await updateName(next);
},
{ error: null }
);
return (
);
}
“`
This is roughly 20 lines. The pre-19 equivalent with race-condition protection, pending state, and rollback on error would have been closer to 70, and half of it would have been subtly wrong.
Tooling: what the ecosystem actually uses in 2026
If you are starting a new React project today, the defaults have converged. Vite is the build tool for unframeworked apps — create-react-app has been formally deprecated since early 2023 and the react.dev docs do not even mention it. For anything with routing or data fetching, the choice is Next.js (App Router), React Router 7 (the post-Remix merger), or TanStack Start. All three support Server Components, all three support streaming, and the decision is mostly about deployment target and how opinionated you want the framework to be.
For testing, Vitest has overtaken Jest in new React projects — it is faster, shares the Vite config, and ships ESM support out of the box that Jest still struggles with. React Testing Library works identically on both. Playwright has all but replaced Cypress for end-to-end tests, largely because its component testing mode handles Server Components correctly while Cypress still struggles with them.
On the type front, TypeScript 5.5+ is the baseline. The main thing to know is that React 19’s types moved ReactElement‘s children default from any to unknown, which will surface a pile of type errors in any codebase that was sloppy about typing children. The automated codemod mentioned earlier handles most of them; the remainder are worth fixing by hand rather than casting away.
The upgrade order that actually works
Teams that have run clean React 19 upgrades tend to do it in this order, not all at once:
- Turn on
eslint-plugin-react-compileron the existing React 18 codebase. Fix the violations. This is the hardest step and the most valuable one — it surfaces real bugs. - Upgrade to React 19 without the compiler enabled. Run the codemods. Fix the
defaultProps, string ref, and type issues. Ship this as its own release. - Enable the React Compiler. Run your full test suite. Watch the build output for components the compiler skipped and decide whether to fix them or live with the skip.
- Start dropping
forwardRef,useCallback, anduseMemoin new code. Don’t mass-rewrite old code — the compiler handles it regardless of whether the source still has the manual memoization.
Skipping step 1 and going straight from 18 to 19-with-compiler is the path I see fail most often. The compiler diagnostics during a build are hard to read in bulk; the ESLint output on a working dev server is not.
The practical takeaway: React 19 plus the compiler is the first React upgrade in years where the right move is “upgrade now, then delete code.” Most of the patterns we wrote to work around React’s reconciler — manual memoization, forwardRef ceremony, effects that sync derived state — are now either automated or unnecessary. The upgrade cost is real, mostly in TypeScript fallout and in rooting out the rule-of-React violations the compiler refuses to touch. Pay it once and your codebase gets smaller, not larger.
