React 19.2 is a minor release, and most of a minor release is safely ignored until you have a reason to care. Three of its additions are worth reading now, because each one replaces a workaround that is already in your codebase: a hand-rolled keep-alive wrapper, a ref-that-holds-the-latest-callback, and a profiling session where you cannot tell React's work apart from everything else on the main thread.
We are not arguing you should upgrade for these. If you are on React 18, the 18-to-19 sequence is the decision; 19.1 to 19.2 is a patch-shaped step afterwards. This is about what to do with the release once you are on it.
<Activity>: hidden UI that is not unmounted
The long-running gap in React was that hiding a subtree and keeping a subtree were the same decision. Conditionally rendering null throws away state, effects, and DOM. Rendering with display: none keeps everything, including effects that keep running, subscriptions that keep firing, and layout cost you are paying for something nobody can see.
<Activity> splits the two:
<Activity mode={isActive ? 'visible' : 'hidden'}>
<SearchPanel />
</Activity>
In hidden mode React hides the DOM, unmounts effects, and keeps the state. Switching back to visible remounts the effects against the state that was already there. Hidden content is also rendered at lower priority than anything visible, so pre-rendering a tab the user has not opened does not compete with the tab they are looking at.
Two uses justify the API in product code. The first is tabbed or wizard-style UI where re-entering a panel currently resets scroll position, form input, or a loaded list, and the team has papered over it by lifting all of that state into a store it does not otherwise belong in. The second is pre-rendering a likely next view — a detail panel, the next step of a checkout — so that the transition is instant. That is the case to be careful with: pre-rendering costs render work and possibly data fetches for a view that may never be shown. Measure the transition you are trying to improve before and after, on a throttled CPU, and keep the change only if the number moved.
The mistake to avoid is treating <Activity> as a general memoization escape hatch — wrapping a slow route in it so it never re-mounts. Hidden trees still hold memory: state, cached data, DOM nodes for anything React has already rendered. In a long session with many hidden panels that is a leak with a nicer name. Keep the set of simultaneously-hidden activities small and bounded.
Effects unmounting on hide is also a real behaviour change from display: none. An effect that opens a WebSocket, starts a poll, or registers an event listener will be torn down and set back up. That is usually what you want, and occasionally it is not — a subscription you intended to hold open across a tab switch now reconnects. Read the effects in a subtree before you wrap it.
useEffectEvent: the last legitimate use of the ref trick
The pattern this replaces is in nearly every codebase we review:
const callbackRef = useRef(onMessage);
useEffect(() => { callbackRef.current = onMessage; });
useEffect(() => {
const socket = connect(roomId);
socket.on('message', (m) => callbackRef.current(m));
return () => socket.close();
}, [roomId]);
It exists because the effect needs the latest onMessage but must not reconnect when onMessage changes identity. The ref is a manual escape from the dependency array, and the lint rule cannot tell whether you did it correctly.
19.2 stabilises the intended API:
const onMessageEvent = useEffectEvent((m) => onMessage(m));
useEffect(() => {
const socket = connect(roomId);
socket.on('message', onMessageEvent);
return () => socket.close();
}, [roomId]);
onMessageEvent always sees the current props and state, is not reactive, and is not listed as a dependency. The rules are narrow and worth stating plainly: effect events may only be called from inside effects, they must be declared in the component or hook that uses them, and they must not be passed to child components or into other hooks' dependency lists. The linter enforces this, which is the point — the ref version enforced nothing.
When you adopt it, resist a sweeping refactor. The effects worth converting are the ones with a suppressed exhaustive-deps comment or a latestRef next to them; those comments are a search away, and each conversion is small and independently reviewable. Effects that are genuinely over-firing because of a dependency you should have memoized are a different bug, and useEffectEvent will hide it rather than fix it.
React performance tracks in the Chrome profiler
The third addition changes how we profile. Recording a performance trace in Chrome DevTools on a React 19.2 app now shows two extra tracks: scheduler work by priority, and the component tree for the render being performed, both aligned to the same timeline as scripting, layout, paint, and long tasks.
This matters because the React DevTools Profiler and the Chrome profiler have always answered different questions, and neither answered them together. React DevTools told you which component re-rendered and how long its commit took; Chrome told you that the main thread was blocked for 340 ms but not by what. Putting React's own phases on the browser timeline means you can see that a 300 ms interaction was 40 ms of React render and 260 ms of a synchronous layout read triggered by a measurement hook — which is a different fix from anything memoization would give you.
Our working order has not changed, just got easier to execute. Field data first: CrUX or your RUM tells you which interactions are actually slow for real users. Then reproduce the interaction locally with CPU throttling on, record a Chrome trace, and read the React tracks to attribute the time. Then change one thing and re-record. A fix that cannot be shown in a before-and-after trace is a guess, and guesses accumulate in a codebase as memoization nobody can safely remove.
Two caveats. The tracks reflect a development or profiling build's timing characteristics, so treat the shape as diagnostic and the absolute milliseconds as indicative; confirm the win in production field data. And if you have adopted the React Compiler, re-profile after adopting it rather than before — the compiler changes which components re-render, so a hotspot list from last quarter may no longer describe your app.
What we would actually do
On an app already on React 19, upgrading to 19.2 is a version bump and a test run, and the three features above are adopted independently, in whatever order your bug list justifies. There is no migration here.
On an app on 18 or older, none of this changes the priority. Get to 19 as shippable increments — types first, runtime second, deprecated APIs on their own commits — and treat <Activity> and useEffectEvent as things you will be glad to have afterwards, not reasons to compress the upgrade. If your current pain is a tab that loses its state, the honest answer is that a small amount of lifted state fixes it today, and <Activity> deletes that code later.