React 19.2: Activity, useEffectEvent, and the React Compiler - A Practical Deep Dive
Author
Muhammad Awais
Published
June 1, 2026
Reading Time
16 min read
Views
22k

React 19.2 - What Actually Changed and Why It Matters
React 19.2 dropped in October 2025, and it's the most practically significant React release since 19.0. Not because of a single headline feature, but because three things shipped simultaneously that together change how you should structure React applications in 2026. The official React 19.2 announcement covers the API surface this guide goes deeper on the practical patterns and the cases where you shouldn't use these features.
Most articles about this release describe what these features are. This guide goes further it explains the exact problems they solve, shows the before/after in real code, and tells you when not to use them, which is usually the part that's missing. If you've read three blog posts about React 19.2 and still aren't sure whether to use <Activity /> or just {isVisible && <Component />}, keep reading.
This builds on the foundational React 19 features covered in our React 19 complete features guide if you're not yet up to speed on Actions, useActionState, and useFormStatus, start there first.
The
<Activity />component what it does, what it doesn't do, and when conditional rendering is still betterHow
useEffectEventpermanently fixes the stale closure problem inuseEffectReact Compiler 1.0 what "stable" actually means and how to adopt it incrementally
Partial Pre-rendering the PPR model explained with practical Next.js context
The
cacheSignalAPI for Server ComponentsThe two security CVEs you need to patch if you haven't already
Upgrade path: what breaks, what doesn't, and in what order to do it
The Activity Component - Keeping State Without Keeping the Component Visible
<Activity /> solves a specific problem that comes up constantly in complex UIs: you want to hide part of your app, but you don't want to lose its state when it's hidden.
The pattern before React 19.2:
// Before — unmounts completely, state lost
{isVisible && <HeavyComponent />}
// Or — keeps state but visible in DOM (accessibility problem)
<div style={{ display: isVisible ? 'block' : 'none' }}>
<HeavyComponent />
</div>Neither option is great. The first loses state every time you toggle visibility expand a tree, hide it, re-show it and everything is collapsed again. The second keeps state but the component remains in the DOM, accessible to screen readers, indexed by search engines, and consuming resources even when "hidden."
React 19.2's <Activity />:
import { Activity } from 'react';
// Hidden: children removed from DOM, effects unmounted,
// BUT state is preserved for when it becomes visible again
<Activity mode={isVisible ? 'visible' : 'hidden'}>
<HeavyComponent />
</Activity>In hidden mode: the children are removed from the rendered output (not in the DOM, not accessible to screen readers), effects are unmounted, and updates are deferred until React has nothing else to process. In visible mode: children render normally, effects mount, updates process at normal priority.
The key thing that makes <Activity /> different from both alternatives: component state is preserved across visibility changes. A <HeavyComponent /> in hidden mode remembers its scroll position, open/closed accordion state, form field values, and anything else held in React state. When it comes back to visible, it's exactly where the user left it.
When to Use Activity - And When NOT To
Good use cases for <Activity />:
Tab panels where users switch back and forth frequently and expect their state preserved
Sidebars, drawers, and panels that users open and close repeatedly during a session
Multi-step forms where previous steps should retain their values
Split-panel editors where the secondary panel is temporarily hidden
When to still use conditional rendering instead:
When the component should reset on hide: A modal confirmation dialog should always start fresh when opened. Using
<Activity />would preserve whatever state it was in last time that's wrong for a confirmation modal.When the content rarely needs to be shown again: If a user dismisses something and will probably never see it again, the overhead of keeping state around isn't worth it. Conditional rendering is cheaper.
When the component is small and cheap to remount: The entire point of
<Activity />is avoiding expensive remount costs. For a simple component with no state, conditional rendering is identical in performance.When you're not in a React 19.2+ environment: Activity isn't available in React 19.0 or 19.1. Check your version before using it.
useEffectEvent - The Permanent Fix for Stale Closures in useEffect
This is the one that I'm most excited about in React 19.2, because it solves a problem every React developer has hit and worked around poorly.
The problem: you have a useEffect that sets up a connection or subscription. Inside it, you need to use some value like a theme, a user preference, or a callback prop but that value shouldn't trigger the effect to re-run when it changes. The effect should only re-run when the actual connection dependency changes.
// The classic problem
function ChatRoom({ roomId, theme }) {
useEffect(() => {
const connection = createConnection(roomId);
connection.connect();
// We want to use theme for notifications,
// but we don't want the effect to reconnect every time theme changes
connection.on('message', (msg) => {
showNotification(msg, theme); // <-- theme in dependency array = reconnects on theme change
});
return () => connection.disconnect();
}, [roomId, theme]); // theme here causes unnecessary reconnects
}Before React 19.2, the "solutions" were all bad:
Add
themeto the dependency array → effect reconnects on every theme change (wasteful)Remove
themefrom the dependency array → stale closure, always uses the initial theme value (buggy)Use a ref to store theme → works but adds boilerplate and is easy to get wrong
Disable the ESLint rule → creates tech debt and suppresses a warning that's usually right
useEffectEvent gives you a proper solution:
import { useEffect, useEffectEvent } from 'react';
function ChatRoom({ roomId, theme }) {
// useEffectEvent creates a stable function that always sees
// the latest theme value without being a dependency
const onMessage = useEffectEvent((msg) => {
showNotification(msg, theme); // Always reads latest theme — no stale closure
});
useEffect(() => {
const connection = createConnection(roomId);
connection.connect();
connection.on('message', onMessage); // onMessage is NOT in the dependency array
return () => connection.disconnect();
}, [roomId]); // Only reconnects when roomId changes — correct!
}The mental model: useEffectEvent creates an "event handler" a function that always reads current values but isn't reactive. It can be called inside an effect, but it doesn't trigger the effect to re-run when its internal values change.
useEffectEvent Rules - What You Can and Cannot Do
There are specific constraints that matter:
Never call a useEffectEvent function outside of an effect. It's only meant to be called from within
useEffector event handlers inside effects. Calling it at the top level or from a render function breaks the model.Never pass a useEffectEvent function to other components. Don't spread it as a prop or put it in context. It's an implementation detail of the effect, not a public API.
The ESLint plugin will enforce these rules. React 19.2 ships updated ESLint rules that flag misuse of
useEffectEvent. Don't suppress them.
A practical audit: go through your codebase and find every place you have a comment like // eslint-disable-next-line react-hooks/exhaustive-deps. A meaningful portion of those are legitimate candidates for useEffectEvent. Check if the suppressed dependency is something that shouldn't actually trigger the effect if yes, this is your fix.
For deeper patterns on effects and performance including when to avoid effects entirely our guide to fixing unnecessary re-renders in React covers the mental model that makes useEffectEvent fit naturally.
React Compiler 1.0 - What "Stable" Actually Means
React Compiler was announced as experimental in 2024 and reached 1.0 stable in 2026. A lot of developers are still confused about what it does, what it doesn't do, and whether they should use it. Let me be direct.
What React Compiler does: It automatically adds memoization to your components and hooks the equivalent of wrapping things in useMemo, useCallback, and React.memo where the compiler can prove it's safe. It transforms your code at build time into an optimized version.
// You write this:
function ExpensiveList({ items, filter }) {
const filtered = items.filter(item => item.type === filter);
return filtered.map(item => <Item key={item.id} {...item} />);
}
// Compiler generates something equivalent to:
const ExpensiveList = React.memo(function ExpensiveList({ items, filter }) {
const filtered = useMemo(
() => items.filter(item => item.type === filter),
[items, filter]
);
return filtered.map(item => <Item key={item.id} {...item} />);
});What React Compiler does NOT do:
It doesn't fix broken React code if you violate the rules of hooks or have side effects in render, the compiler doesn't help, it may make things worse
It doesn't replace understanding React's rendering model you still need to understand why things re-render
It doesn't make every app faster the memoization overhead is only worth it for expensive renders
It doesn't work without the babel plugin or the Vite/Next.js integration
How to Adopt React Compiler Incrementally
The React team specifically designed Compiler 1.0 for gradual adoption. Don't enable it globally on day one use the opt-in approach:
// babel.config.js — enable compiler with opt-in mode
module.exports = {
plugins: [
['babel-plugin-react-compiler', {
compilationMode: 'annotation', // Only compiles files with the directive
}],
],
};
// In specific component files — add the directive to opt in
'use memo'; // This file will be compiled
export function MyComponent({ data }) {
// Compiler automatically optimizes this
const processed = data.map(item => ({ ...item, processed: true }));
return <List items={processed} />;
}Start with your most performance-sensitive components the ones where you already have manual useMemo and useCallback calls. Enable the compiler on those files, verify the output with React DevTools, and expand from there.
For Next.js 15+, enable via next.config.ts:
// next.config.ts
const nextConfig = {
experimental: {
reactCompiler: {
compilationMode: 'annotation', // Start with opt-in
},
},
};
export default nextConfig;The ESLint plugin (eslint-plugin-react-compiler) will flag any code patterns that violate the rules the compiler requires. Run it before enabling the compiler fix all violations first, or the compiler will skip those components silently.
Partial Pre-rendering : The PPR Model Explained
Partial Pre-rendering (PPR) was experimental in Next.js 14 and is now stable in React 19.2 / Next.js 15. It's one of those features that sounds complicated but makes complete sense once you understand the problem it solves.
Standard server-side rendering: the entire page waits for all data before rendering. One slow database query holds up the whole page.
Standard streaming with Suspense: dynamic parts stream in as they resolve. Better, but the static shell still needs to be server-rendered.
PPR: the static parts of your page are pre-rendered at build time (like a static site). The dynamic parts the parts wrapped in Suspense are streamed in at request time. Users see the static shell instantly (from CDN), then dynamic content fills in.
// app/page.tsx — PPR in practice with Next.js 15
import { Suspense } from 'react';
export default function Page() {
return (
<main>
{/* This is pre-rendered at build time — served from CDN instantly */}
<StaticHeader />
<StaticHero />
{/* This streams in at request time — doesn't block the static content */}
<Suspense fallback={<PersonalizedSkeleton />}>
<PersonalizedFeed userId={userId} />
</Suspense>
<StaticFooter />
</main>
);
}The practical impact: pages with PPR score dramatically better on LCP (Largest Contentful Paint) because the static shell usually the largest visual element on screen arrives from CDN in under 100ms. The dynamic content streams in without blocking the initial paint.
Enable PPR in Next.js 15 via next.config.ts:
const nextConfig = {
experimental: {
ppr: 'incremental', // Enable per-route with export const experimental_ppr = true
},
};
// Then in individual route files:
export const experimental_ppr = true;cacheSignal - Cancelling Stale Server Component Requests
cacheSignal is a smaller but practically useful addition for React Server Components. It gives you an AbortSignal that fires when a cached resource is no longer needed useful for cancelling long-running operations that a cached response would have rendered.
import { unstable_cacheSignal as cacheSignal } from 'react';
async function fetchUserData(userId: string) {
const signal = cacheSignal(); // Get signal for this cache entry
const response = await fetch(`/api/users/${userId}`, {
signal, // If cache entry is invalidated before fetch completes, it cancels
});
return response.json();
}Without cacheSignal, stale Server Component requests would continue running even after a revalidation made them irrelevant wasting server resources. This is particularly useful in apps with frequent revalidations or real-time data patterns. It works with the Next.js revalidatePath and revalidateTag patterns to clean up in-flight requests when cache is invalidated.
Security Patches - Apply These If You Haven't
React 19.2 also contains critical security fixes. In December 2025, two CVEs were disclosed affecting React Server Components:
CVE-2025-55182 (React2Shell): An unauthenticated remote code execution vulnerability in React Server Components. Affects React 19.0.0 through 19.2.2. Patched in React 19.2.3+.
RSC source code exposure: A separate vulnerability could expose server component source code under specific conditions. Patched simultaneously in the same release.
Check your version:
npm list react
# Should show 19.2.3 or higherIf you're on any version below 19.2.3 and using React Server Components, update immediately:
npm install react@latest react-dom@latestThese aren't theoretical vulnerabilities they were disclosed with working exploit demonstrations. Don't delay patching.
The Upgrade Path - In the Right Order
React 19.2 has no breaking changes in the traditional sense, but it ships stricter ESLint rules and the security patches require an update. Here's the order that minimizes surprises:
Update React first.
npm install react@19.2.3 react-dom@19.2.3 @types/react@latest @types/react-dom@latestRun your tests. React 19.2 is backward-compatible with 19.0 code nothing should break at this step.
Update the ESLint plugin.
npm install eslint-plugin-react-hooks@latestThe updated rules flag
useEffectEventmisuse and tighten some existing rules. Run ESLint and fix any new warnings before doing anything else.Audit your useEffect dependencies. Before adopting
useEffectEvent, run a search for// eslint-disable-next-line react-hooks/exhaustive-depsin your codebase. For each one, evaluate whetheruseEffectEventis the right fix or whether the suppression was intentional for a different reason.Adopt Activity selectively. Identify 2-3 components where state loss on hide is a current problem tab panels, sidebars, multi-step forms. Add
<Activity />there first and verify behavior before expanding.React Compiler last, incrementally. Don't enable React Compiler globally in the first pass. Use
compilationMode: 'annotation'and add'use memo'to specific high-value files. Verify with React DevTools Profiler that the compiler is making things better, not worse.
Frequently Asked Questions
What is the Activity component in React 19.2?
<Activity /> is a new React component that lets you hide parts of your UI while preserving their state. In mode="hidden", the children are removed from the DOM and effects are unmounted, but React keeps the component's state in memory. When the mode switches back to visible, the component appears exactly as the user left it no state reset, no scroll position lost. It's designed for tabs, sidebars, drawers, and any UI where users switch back and forth and expect their context to be remembered.
What problem does useEffectEvent solve?
useEffectEvent solves the stale closure problem in useEffect. When you need to use a value inside an effect (like a callback prop or a theme setting) but don't want that value to cause the effect to re-run when it changes, useEffectEvent wraps that logic in a stable function that always reads the latest value without being a reactive dependency. Before React 19.2, developers worked around this by disabling ESLint rules, using refs, or accepting incorrect behavior.
What is the difference between Activity and conditional rendering?
Conditional rendering ({isVisible && <Component />}) unmounts the component completely when the condition is false state is lost, effects are cleaned up, and on re-mount the component starts fresh. <Activity mode="hidden"> removes the component from the DOM but preserves its state. The right choice depends on whether you want state preserved: use <Activity /> when you want users to return to where they left off, use conditional rendering when you want a fresh start.
Should I enable React Compiler 1.0 on my existing app?
Not globally, not yet. Use compilationMode: 'annotation' and enable it file by file on your most performance-critical components first. Before enabling it on any file, run eslint-plugin-react-compiler and fix all violations the compiler requires your code to follow the rules of React, and violations cause it to silently skip those components. For new projects, enabling it globally from the start is reasonable. For existing large codebases, incremental adoption over several sprints is safer.
What is Partial Pre-rendering and how is it different from SSR?
Standard SSR renders the entire page on the server at request time fast for users but all data must be ready before anything is sent. Partial Pre-rendering (PPR) pre-renders the static parts of your page at build time (served from CDN instantly) and streams the dynamic parts at request time using Suspense. The result: users see the page shell almost immediately from CDN, and personalized or data-driven content fills in without blocking the initial paint. It combines the speed of static generation with the personalization of server rendering.
What are the security vulnerabilities in React 19 and am I affected?
Two CVEs were disclosed in December 2025 affecting React Server Components: CVE-2025-55182 (remote code execution) and a source code exposure vulnerability. Both affect React 19.0.0 through 19.2.2. If you're using React Server Components and haven't updated to 19.2.3 or later, you're potentially vulnerable. Run npm list react to check your version. Update with npm install react@latest react-dom@latest immediately if you're below 19.2.3.
Is useEffectEvent available in React 19.0 or only 19.2?
useEffectEvent is available starting with React 19.2. It was experimental in earlier builds but is now stable and production-ready. If you're on React 19.0 or 19.1, you'll need to upgrade to access it. The upgrade to 19.2.3 is a drop-in change with no breaking changes to existing code it's worth doing for useEffectEvent alone, plus you get the security patches.
Continue Reading
View All HubLevel Up Your Workflow
Free professional tools mentioned in this article
QR Code Generator
Generate custom QR codes for URLs, WiFi, WhatsApp, vCard & more. Add a logo, pick a frame, download PNG/SVG/JPG. Free, no watermark, no signup.
Stripe & PayPal Fee Calculator
Calculate the exact Stripe and PayPal transaction fees for US and UK markets. A free developer tool to estimate SaaS payouts, merchant costs, and revenues.
JWT Decoder & Verifier
Decode, parse, and verify JWT (JSON Web Tokens) securely in your browser. Validate claims and debug authentication payloads instantly with zero server logs.
SVG Path Builder & Visualizer
An interactive, client-side SVG path builder and visualizer tool. Generate optimized cubic and quadratic Bezier vector code instantly on a grid canvas.




