React 19.2 shipped on October 1, 2025, the third release in under a year after 19.0 in December and 19.1 in June. I upgraded a side project the same week and kept notes on what was useful versus what was noise. This is that field guide: the features I now reach for, with the smallest snippet that makes each one click.
1. <Activity> for state you do not want to throw away
The pattern most of us still ship is {isVisible && <Page />}. It works, but the moment a tab goes away, its state, scroll position, and in-flight requests die with it. <Activity> keeps the subtree alive while hiding it.
// Before: state is destroyed when the tab hides
{tab === 'profile' && <ProfilePanel />}
// After: state survives, effects unmount, updates defer
<Activity mode={tab === 'profile' ? 'visible' : 'hidden'}>
<ProfilePanel />
</Activity>
There are two modes in 19.2. hidden unmounts effects and defers updates until React has nothing else to do, so a hidden subtree never steals time from what is on screen. visible mounts effects and renders normally. The payoff: back navigation keeps form inputs, and you can pre-render the screen a user is likely to hit next while the current one stays smooth. Mux wrote up using it to keep video players warm across navigation, which is the kind of thing that used to need a custom keep-alive hack.
2. useEffectEvent to stop lying to the dependency array
This is the one I was waiting for. The old problem: you read a value inside an effect that is not really part of the effect's reactive logic, and now changing that value re-runs the whole thing.
function ChatRoom({ roomId, theme }) {
const onConnected = useEffectEvent(() => {
showNotification('Connected!', theme); // always the latest theme
});
useEffect(() => {
const conn = createConnection(serverUrl, roomId);
conn.on('connected', () => onConnected());
conn.connect();
return () => conn.disconnect();
}, [roomId]); // theme is NOT a dependency, and that is correct now
}
Before this, changing theme reconnected the chat room, which is absurd. The usual fix was to disable the lint rule and drop the dependency, which quietly breaks the linter's ability to catch real mistakes later. useEffectEvent splits the "event" part out so it always sees fresh props and state without being a dependency. Two rules to remember: do not put the effect event in the dependency array, and declare it in the same component or hook as its effect. The linter enforces both once you move to eslint-plugin-react-hooks v6.
The mental model: if a function is conceptually an event that happens to fire from inside an effect, it is an effect event. Do not wrap everything in it just to silence a warning.
Here is a short, honest take on whether it earns its keep:
3. Partial pre-rendering for the static-shell, dynamic-fill split
React 19.2 lets you pre-render the static parts of an app, serve that shell from a CDN, and resume rendering later to fill in the dynamic bits.
// Build time: render what is static, save the rest
const { prelude, postponed } = await prerender(<App />, {
signal: controller.signal,
});
await savePostponedState(postponed);
// Request time: resume into an SSR stream
const postponed = await getPostponedState(request);
const stream = await resume(<App />, postponed);
There is also resumeAndPrerender if you want static HTML for SSG instead of a live stream. This is plumbing most people will get through a framework rather than call by hand, but knowing the shape helps when you are debugging why a route hydrates the way it does.
4. Performance Tracks in Chrome DevTools
19.2 adds custom React tracks to Chrome performance profiles. The Scheduler track shows what React is working on by priority, so blocking work from a click is visually separated from transition work inside startTransition. The Components track shows the tree React is rendering or running effects on, with labels like "Mount" and "Blocked." I used it to find a transition that was being starved by a high-priority render, which would have been guesswork before.
5. Batched Suspense reveals during SSR
A quieter fix that changes how pages feel. Previously, server-streamed Suspense boundaries popped in one by one as each resolved. Now React batches reveals for a short window so more content appears together, matching client behavior and setting up cleaner View Transitions. The safeguard worth knowing: if total load time approaches the 2.5s LCP threshold, React stops batching and reveals immediately rather than hurt your Core Web Vitals.
6. Smaller changes that bite if you ignore them
A few notable items from the changelog:
| Change | Why you care |
|---|---|
Default useId prefix is now _r_ | Old :r: and «r» were invalid for view-transition-name; update any hardcoded ID assertions in tests |
eslint-plugin-react-hooks v6 | Flat config by default, opt-in React Compiler rules; needed for useEffectEvent linting |
| Web Streams SSR for Node | Available now, but Node Streams are still faster and support compression, so prefer them |
That last row is a trap. Web Streams in Node sound modern, but the React team explicitly recommends renderToPipeableStream because Web Streams skip compression by default and you can silently lose the benefit of streaming.
What I would actually adopt first
If you upgrade today, useEffectEvent is the change that pays off immediately and lets you delete a pile of lint-rule suppressions. <Activity> is the one to design around next, because it quietly removes a whole category of "save the state before this unmounts" code. The rest is worth reading once so you recognize it when your framework starts leaning on it. My bet for the next year: Activity plus View Transitions becomes the default way we build navigation, and the hand-rolled keep-alive component finally goes extinct.
