
React 19.2 shipped in October 2025. Most teams are still writing useCallback chains and display: none CSS hacks. Two new APIs — <Activity /> and useEffectEvent — directly fix the two most persistent React bugs that have survived every major version. If your app has tabs, modals, or real-time subscriptions, this is the upgrade that actually changes how you work.
The Tab State Problem
Every React developer has hit this. You build a tabbed interface, the user fills out a form on the Home tab, switches to Settings, then switches back. The form is blank. React unmounted the component. All local state — inputs, scroll position, fetched data — is gone.
The classic workarounds are both bad. CSS display: none keeps everything mounted and running, wasting resources on effects and re-renders you cannot see. Lifting state to the parent solves the data problem but adds complexity and breaks encapsulation. Neither approach is satisfying.
How <Activity /> Fixes It
The new <Activity> component keeps a component mounted and preserves all of its state, but pauses its effects and defers updates when hidden. State survives. Effects do not run unnecessarily. It is the behavior you wanted from display: none without the resource waste.
import { Activity, useState } from 'react';
function Tabs() {
const [active, setActive] = useState('home');
return (
<>
<nav>
<button onClick={() => setActive('home')}>Home</button>
<button onClick={() => setActive('settings')}>Settings</button>
</nav>
<Activity mode={active === 'home' ? 'visible' : 'hidden'}>
<HomeTab />
</Activity>
<Activity mode={active === 'settings' ? 'visible' : 'hidden'}>
<SettingsTab />
</Activity>
</>
);
}
When a tab is hidden, all useState and useReducer values are preserved. The DOM nodes stay in the tree. When the tab becomes visible again, effects resume — no cold restart, no re-fetch, no lost form data. The official Activity API reference documents both visible and hidden modes in full.
One trade-off to keep in mind: hidden components stay in memory. For most tabbed UIs this is the right call — the state is small and the re-initialization cost you are avoiding is real. For rarely visited heavy panels, you may still prefer to unmount. The decision is now yours instead of being forced on you.
The Stale Closure Problem
The other fix is arguably even more impactful. The stale closure problem in useEffect is one of the most frequently searched React questions on Stack Overflow. Here is the canonical form:
useEffect(() => {
const connection = connectToRoom(roomId);
connection.on('message', () => {
if (!isMuted) playSound(); // BUG: isMuted is stale from initial render
});
return () => connection.disconnect();
}, [roomId]); // isMuted is missing, but adding it causes the effect to re-run on every toggle
Adding isMuted to the dependency array fixes the stale value but makes the effect reconnect every time the user toggles mute — which is wrong. Using a useRef wrapper works but is boilerplate. useCallback pushes the problem up one level. None of these solutions are clean.
How useEffectEvent Fixes It
useEffectEvent creates a function that always reads the latest props and state when called, but is not reactive — it does not trigger the effect to re-run when the values it reads change. The useEffectEvent documentation covers the full mental model.
import { useEffect, useEffectEvent } from 'react';
function ChatRoom({ roomId }) {
const [isMuted, setIsMuted] = useState(false);
const onMessage = useEffectEvent(() => {
if (!isMuted) playSound(); // Always reads latest isMuted — no stale closure
});
useEffect(() => {
const connection = connectToRoom(roomId);
connection.on('message', onMessage);
return () => connection.disconnect();
}, [roomId]); // onMessage is NOT in the dep array — this is correct
}
The effect reconnects when roomId changes, which is the right behavior. It does not reconnect when the user toggles mute. The callback inside the effect always sees the current value of isMuted. Problem solved with no hacks.
Three Rules for useEffectEvent
- Only call it from inside a
useEffect— never during render - Never pass it as a prop to child components
- Do not use it just to silence ESLint exhaustive-deps warnings — that is a code smell
The third rule matters. useEffectEvent is for logic that is genuinely event-like: a notification sound, an analytics call, a logging side effect. If your logic should be reactive, keep it in the dependency array. LogRocket’s useEffectEvent deep dive has more on distinguishing reactive from non-reactive logic.
What Else Shipped in React 19.2
React DevTools Performance Tracks: Chrome DevTools now shows React’s scheduler lanes — blocking, transition, idle — as separate tracks in the Performance tab. Finding slow interactions just got easier. Requires DevTools extension 5.4+.
SSR Suspense batching: React now batches the reveal of server-rendered Suspense boundaries for a short window, matching client-rendered behavior and eliminating a class of layout shifts in streamed Next.js apps. LCP safeguards are built in — batching stops if total load time approaches 2.5 seconds.
cacheSignal: An AbortSignal for React Server Component developers using cache(). Lets you cancel in-flight fetches when the cache entry expires. Niche but necessary for complex RSC trees.
Upgrading to React 19.2
No breaking changes in React 19.2. Run npm install react@19 react-dom@19 and you get all of the above. The full release notes are on the official React 19.2 blog post. If you are on Next.js 16, both <Activity /> and useEffectEvent are available in the App Router today — the Next.js 16 release ships React 19.2 by default.
The React Compiler (stable since October 2025) handles memoization automatically — these two APIs address what the compiler cannot: runtime state preservation and event-handler staleness inside effects. They are complementary, not redundant.
React 19.2 will not rewrite your architecture. But if you have ever lost a user’s form data on a tab switch, or spent an afternoon debugging a stale closure in an effect, these two additions are exactly what you have been waiting for.













