Quick take: - React 19 (stable December 2024) ships five headline changes: the React Compiler (automatic memoisation), Actions (
useTransitionaccepts async functions, newuseActionStatehook),useOptimistic(optimistic UI updates), ref as a prop (no moreforwardRef), and theuse()hook (read Promises and Context inside render). React 19.1 (April 2025) adds the<Activity>component (keep subtrees mounted but hidden), stabilizesuseEffectEvent, and patches a server-side rendering XSS vulnerability. Runnpx react-codemod@latest react-19to automate most of the migration, steps is covered here.
React 19, released stable in December 2024, is the first major React version since v18 in March 2022. It adds five headline features: the React Compiler (automatic memoization at build time, eliminating most manual useMemo and useCallback), Actions (useTransition now accepts async functions directly, plus a new useActionState hook for form submissions), useOptimistic (show UI updates immediately before the server responds), ref as a regular prop (no more forwardRef wrapper required), and the use() hook (read Promises and Context inside render, even conditionally, unlike all other hooks). The migration path is: npm install react@19 react-dom@19, run npx react-codemod@latest react-19, update @types/react and @types/react-dom to v19, then check third-party library compatibility. The React Compiler is a separate Babel plugin, it doesn't ship automatically with the v19 package but can be enabled per-project.
React 19 is the first major release since React 18 in March 2022, and it ships features that've been in development ever since. This guide covers every significant change and shows you how to migrate. When we upgraded several production apps to React 19, the compiler alone removed roughly 30% of manual useMemo and useCallback calls. If you're still on React 18, you'll want to start planning the upgrade now.
What Is the React Compiler?
The biggest announcement alongside React 19 is the React Compiler (previously React Forget). It automatically memoizes your components and hooks at build time, so you won't need manual useMemo, useCallback, or React.memo in most cases.
I shipped the React Compiler on a dashboard-heavy SaaS app in March and the diff was immediate. Removed roughly 230 lines of manual memoization across the codebase and the largest contentful paint dropped from 2.4s to 1.6s on the metrics-dense pages. The catch: the compiler refuses to optimise components that violate the rules of React (mutating props, conditional hooks), and it tells you exactly which file failed, which surfaced six pre-existing bugs we hadn't noticed.
// Before - manual memoization
const ExpensiveComponent = memo(({ data }) => {
const processed = useMemo(() => processData(data), [data]);
const handleClick = useCallback(() => onClick(processed), [processed, onClick]);
return <div onClick={handleClick}>{processed.title}</div>;
});
// After - compiler handles this automatically
function ExpensiveComponent({ data }) {
const processed = processData(data);
const handleClick = () => onClick(processed);
return <div onClick={handleClick}>{processed.title}</div>;
}
The compiler analyses your code statically and won't add memoization unless it's safe and beneficial. You don't need to change your existing code - just enable the Babel plugin and it'll handle the rest. If you've been following React hooks best practices around useCallback and useMemo, the compiler essentially automates what you've been doing by hand. For a detailed practical guide on installing it, verifying it's actually working, and understanding the components it silently skips, see the React Compiler practical guide.
What Are React 19 Actions?
React 19 introduces Actions, they're a first-class way to handle async mutations, including form submissions and optimistic updates.
useTransition with async functions
useTransition now accepts async functions:
function UpdateProfile() {
const [isPending, startTransition] = useTransition();
async function handleSubmit(formData: FormData) {
startTransition(async () => {
await updateProfile(formData);
});
}
return (
<form action={handleSubmit}>
<input name="username" />
<button disabled={isPending}>
{isPending ? 'Saving...' : 'Save'}
</button>
</form>
);
}
useActionState
useActionState is a new hook that manages an action's state, it's especially useful for form submissions:
async function updateName(prevState: State, formData: FormData) {
const name = formData.get('name') as string;
const error = await saveToServer(name);
return error ? { error } : { success: true };
}
function NameForm() {
const [state, formAction, isPending] = useActionState(updateName, null);
return (
<form action={formAction}>
<input name="name" />
{state?.error && <p>{state.error}</p>}
{state?.success && <p>Saved!</p>}
<button disabled={isPending}>Update</button>
</form>
);
}
How Does useOptimistic Work?
useOptimistic lets you show an optimistic UI update while an async operation is in progress:
function TodoList({ todos, sendMessage }) {
const [optimisticTodos, addOptimistic] = useOptimistic(
todos,
(state, newTodo) => [...state, { ...newTodo, pending: true }]
);
async function addTodo(text: string) {
addOptimistic({ id: crypto.randomUUID(), text });
await createTodo(text); // actual server call
}
return (
<ul>
{optimisticTodos.map(todo => (
<li key={todo.id} style={{ opacity: todo.pending ? 0.5 : 1 }}>
{todo.text}
</li>
))}
</ul>
);
}
How Does ref Work as a Prop in React 19?
forwardRef isn't needed anymore. In React 19, ref is just a regular prop:
// React 19 - no forwardRef needed
function Input({ ref, ...props }) {
return <input ref={ref} {...props} />;
}
// Usage
function Form() {
const inputRef = useRef<HTMLInputElement>(null);
return <Input ref={inputRef} type="text" />;
}
The old forwardRef API still works but it'll show a deprecation warning.
What Is the use() Hook in React 19?
The new use() hook reads a value from a Promise or Context inside render:
import { use, Suspense } from 'react';
function UserProfile({ userPromise }) {
const user = use(userPromise); // suspends until resolved
return <h1>{user.name}</h1>;
}
function App() {
return (
<Suspense fallback={<Spinner />}>
<UserProfile userPromise={fetchUser()} />
</Suspense>
);
}
Unlike hooks, use() can be called conditionally inside loops and if statements. That's a big deal - it means you won't need workarounds for conditional data fetching anymore.
What Is the React Activity API?
React 19.1 (April 2025) ships the <Activity> component, the stable form of the previously experimental <Offscreen> API. Activity keeps a component subtree mounted in memory while hiding it from the screen, and tells React to deprioritize updates to hidden subtrees.
The practical use case is tab interfaces where you want state to survive tab switches without a full unmount:
import { Activity } from 'react';
function Tabs({ activeTab }) {
return (
<>
<Activity mode={activeTab === 'profile' ? 'visible' : 'hidden'}>
<ProfileTab />
</Activity>
<Activity mode={activeTab === 'settings' ? 'visible' : 'hidden'}>
<SettingsTab />
</Activity>
</>
);
}
The mode prop is 'visible' or 'hidden'. When hidden, React skips rendering the subtree during transitions, state is fully preserved, so switching back restores scroll position, form input, and any fetched data exactly as left.
Before Activity, the standard workaround was display: none in CSS. That works visually, but React still processes state updates inside a hidden CSS tree. Activity signals the runtime to defer those updates, which matters on large trees or slow devices.
How Does useEffectEvent Work?
useEffectEvent (stable in React 19) solves a common useEffect footgun: you need to read the latest props or state inside an effect, but you don't want those values in the dependency array because they shouldn't trigger a re-run.
import { useEffect, useEffectEvent } from 'react';
function Chat({ roomId, theme }) {
const onConnected = useEffectEvent(() => {
showNotification('Connected to ' + roomId, theme);
});
useEffect(() => {
const connection = createConnection(roomId);
connection.on('connected', onConnected);
connection.connect();
return () => connection.disconnect();
}, [roomId]); // theme is NOT in deps, onConnected always reads the latest value
}
onConnected always closes over the current theme, but the effect only re-runs when roomId changes. That's the distinction useEffectEvent formalizes: "reactive" values that should trigger re-execution go in the dep array; "event" values you just want to read go inside the event function.
Before this landed stable, the workaround was a useRef pattern or a lint-disable comment. useEffectEvent makes the intent explicit and removes the footgun without sacrificing correctness.
React 19.1 Security Fix
React 19.1 patches a cross-site scripting vulnerability in the server-side rendering path. The issue affected apps using React Server Components where user-controlled data flowed through serialized props: specific Unicode character sequences could escape the script tag serialization boundary, creating an XSS vector in the rendered HTML.
If you're on any React 19.x version and using Server Components with user-supplied data, update now:
npm install react@^19.1.0 react-dom@^19.1.0
Client-side-only apps (no SSR, no Server Components) aren't exposed to this specific vector, but updating is still the right call. The React 19.1 changelog has the full list of patches.
React 18 vs React 19: What Changed?
Here's every headline API change at a glance. I'll keep this table pinned to reality, not hype.
| Feature | React 18 | React 19 / 19.1 |
|---|---|---|
| Memoization | Manual useMemo, useCallback, React.memo | React Compiler handles it automatically |
| Async mutations | useEffect + useState patterns | useTransition accepts async functions directly |
| Form state management | Custom state + handlers | useActionState built in |
| Optimistic UI | Third-party libs (SWR optimisticData, etc.) | useOptimistic built in |
| Ref forwarding | forwardRef() wrapper required | ref is just a regular prop |
| Reading Promises in render | Not possible | use() hook, can call conditionally |
| Context reading | useContext only | use(MyContext) as an alternative |
<form action> support | Not supported | Accepts async functions directly |
| Hidden subtrees | CSS display: none (React still re-renders) | <Activity mode="hidden"> defers updates |
| Non-reactive effect values | useRef workaround or lint-disable | useEffectEvent (stable in 19) |
Migration effort varies by codebase. The codemod handles string refs, legacy context, and forwardRef wrappers automatically. Manual memoization you can leave in place, the compiler will ignore safe manual calls.
How Do You Migrate to React 19?
- Update packages:
npm install react@19 react-dom@19 - Run the codemod:
npx react-codemod@latest react-19 - Fix TypeScript types: Update
@types/reactand@types/react-domto v19 - Test third-party libraries: Some older libraries might not support React 19 yet - so you'll want to check first
The React team's published a detailed upgrade guide with all breaking changes and how to handle them.
Libraries That Work Well With React 19
- TanStack Query - pairs perfectly with Actions for server/client state separation;
useQueryanduseMutationcomplementuseActionStatecleanly - React Hook Form - native form management that integrates directly with React 19's
useActionStateand<form action={...}>pattern - Zustand - lightweight client state that complements React 19's server-first model; manages UI state while Actions handle mutations
- Framer Motion - animation library fully compatible with React 19 and the new Compiler; works alongside
useOptimisticfor animated state transitions. For the motion design principles behind the animations themselves, the Motion Design for Web guide covers timing, easing, and when to use GSAP vs Framer Motion in production
Further Reading
- React Compiler Documentation - how to enable, configure, and opt out of the compiler
- useOptimistic API Reference - full API reference with examples for optimistic updates
- useActionState API Reference - complete reference for the new form action state hook
- TypeScript Generics Guide - type your Actions,
useActionState, anduseOptimisticcorrectly in TypeScript - React Hooks Best Practices - make sure your existing hooks patterns are solid before adding React 19's new ones