Updated January 2026: This guide now includes critical security patches for CVE-2025-55182 (React2Shell) and related vulnerabilities. If you're running React 19.0.0 through 19.2.2, update immediately.
React 19.2 marks the third major release within a year, bringing significant new features and improvements to the React ecosystem. This release introduces:
- The
<Activity>component to manage visibility and preserve state - The
useEffectEventhook for stable event handlers in effects - The
cacheSignalAPI for React Server Components - Partial Pre-rendering for hybrid static/dynamic rendering
- Chrome DevTools Performance Tracks
- SSR streaming improvements
In general, there are no breaking changes in this release, but there are stricter ESLint rules and critical security patches you need to apply.
Critical Security Update (December 2025)
In December 2025, multiple critical vulnerabilities were disclosed affecting React Server Components. If you're using React 19.0.0 through 19.2.2, your application may be vulnerable to remote code execution.
Vulnerabilities Summary
| CVE | Severity | CVSS | Impact |
|---|---|---|---|
| CVE-2025-55182 | Critical | 10.0 | Remote Code Execution (React2Shell) |
| CVE-2025-55184 | High | 7.5 | Denial of Service |
| CVE-2025-55183 | Medium | 5.3 | Source Code Exposure |
| CVE-2025-67779 | High | 7.5 | DoS (incomplete fix) |
Who Is Affected
You're affected if your application:
- Uses React Server Components (RSC)
- Uses any framework supporting RSC: Next.js, React Router, Waku, or similar
- Includes
react-server-dom-webpack,react-server-dom-parcel, orreact-server-dom-turbopack
Default create-next-app configurations are vulnerable. Even apps without explicit Server Functions are at risk if they support RSC.
You're NOT affected if:
- Your app is client-only (no server rendering)
- You don't use any framework or bundler supporting RSC
How to Check Your Version
For Next.js users: Next.js bundles its own forked version of React internally. The version shown in npm list react is not what Next.js actually uses. What matters is your Next.js version, not your react package version.
Check your Next.js version:
npm list nextIf your Next.js version is below the patched versions in the table above (14.2.35, 15.0.7, 15.1.11, 15.2.8, or 16.0.10), you need to update.
For other frameworks (React Router, Waku, etc.): Check your react-server-dom-* packages:
npm list react-server-dom-webpack react-server-dom-parcel react-server-dom-turbopackVulnerable output (needs update):
└── react-server-dom-webpack@19.2.0Patched output (safe):
└── react-server-dom-webpack@19.2.3If you see versions 19.0.0 through 19.2.2 on any react-server-dom-* package, you need to update.
Patched Versions
React packages: 19.0.3, 19.1.4, or 19.2.3
Next.js (by version line):
| Your Version | Update To |
|---|---|
| 13.3.x, 13.4.x, 13.5.x, 14.x | 14.2.35 |
| 15.0.x | 15.0.7 |
| 15.1.x | 15.1.11 |
| 15.2.x | 15.2.8 |
| 16.0.x | 16.0.10 |
How to Patch
For Next.js users (most common): Update Next.js to the patched version for your version line. This is all you need to do since Next.js bundles its own React.
# For Next.js 16.xnpm install next@16.0.10# For Next.js 15.2.xnpm install next@15.2.8# For Next.js 15.1.xnpm install next@15.1.11# For Next.js 14.x or 13.3+npm install next@14.2.35Or use the automated fix that detects your version:
npx fix-react2shell-nextFor other frameworks (React Router, Waku, etc.): Update the React packages directly:
npm install react@19.2.3 react-dom@19.2.3npm install react-server-dom-webpack@19.2.3After Patching
Rotate all application secrets. The RCE vulnerability means attackers may have accessed your server. Treat any secrets in environment variables or hardcoded in Server Functions as potentially compromised.
WAF rules are not sufficient protection. Attackers can modify payloads to bypass signature-based blocking. The only reliable fix is upgrading.
For full details, see the React Security Advisory and follow-up disclosure.
Activity Component
The <Activity> component controls the visibility of a component tree while preserving its state. This is different from conditional rendering, which destroys and recreates components.
Before React 19.2:
{currentTab === 'home' && <HomePage />}With React 19.2:
<Activity mode={currentTab === 'home' ? 'visible' : 'hidden'}> <HomePage /></Activity>The key difference: <Activity> preserves component state when hidden. The ternary approach unmounts the component entirely, losing all state.
When to Use Activity
Use <Activity> when you want to:
- Pre-render likely destinations: Load the next tab's content in the background while the user views the current tab
- Preserve form state: Keep a partially-filled form intact when the user navigates away and back
- Improve perceived performance: Hide components without the cost of remounting them
How Hidden Mode Works
When mode="hidden":
- Children are hidden from the DOM
- Effects are unmounted (cleanup runs)
- Updates are deferred until React has nothing else to process
- State is preserved in memory
When mode="visible":
- Children render normally
- Effects mount
- Updates process at normal priority
Practical Example: Tabbed Interface
import { Activity } from 'react';function TabbedDashboard() { const [activeTab, setActiveTab] = useState('overview'); return ( <div> <nav> <button onClick={() => setActiveTab('overview')}>Overview</button> <button onClick={() => setActiveTab('analytics')}>Analytics</button> <button onClick={() => setActiveTab('settings')}>Settings</button> </nav> <Activity mode={activeTab === 'overview' ? 'visible' : 'hidden'}> <OverviewTab /> </Activity> <Activity mode={activeTab === 'analytics' ? 'visible' : 'hidden'}> <AnalyticsTab /> </Activity> <Activity mode={activeTab === 'settings' ? 'visible' : 'hidden'}> <SettingsTab /> </Activity> </div> );}Each tab maintains its state. If a user starts filling out a form in Settings, switches to Analytics, then returns to Settings, their form data is still there.
For comprehensive documentation, see the official React Activity reference.
useEffectEvent Hook
The useEffectEvent hook creates a stable function that always accesses the latest props and state without triggering effect re-runs.
The Problem It Solves
Consider this common pattern:
function ChatRoom({ roomId, theme }) { useEffect(() => { const connection = createConnection(roomId); connection.on('connected', () => { showNotification('Connected!', theme); }); connection.connect(); return () => connection.disconnect(); }, [roomId, theme]); // theme causes reconnection!}The effect depends on theme only for the notification, but any theme change reconnects the chat. That's wasteful.
The Solution
import { useEffectEvent } from 'react';function ChatRoom({ roomId, theme }) { const onConnected = useEffectEvent(() => { showNotification('Connected!', theme); }); useEffect(() => { const connection = createConnection(roomId); connection.on('connected', onConnected); connection.connect(); return () => connection.disconnect(); }, [roomId]); // theme no longer in dependencies}onConnected always sees the current theme value, but changing theme doesn't re-run the effect.
Rules for useEffectEvent
- Only call Effect Events from inside Effects
- Don't pass them to other components or hooks
- Define them right before the Effect that uses them
- Don't add them to dependency arrays (the ESLint plugin knows this)
When to Use It
Use useEffectEvent when your effect needs to read a value but shouldn't re-run when that value changes. Common cases:
- Logging or analytics that read current state
- Notifications that use current theme/locale
- Callbacks that need fresh values but aren't the "reason" for the effect
In Makerkit, I migrated several functions to useEffectEvent, which cleaned up dependency arrays and reduced unnecessary effect re-runs.
Homework: Audit your useEffect hooks. If you have dependencies that don't logically trigger the effect, consider extracting that logic into a useEffectEvent.
See the official useEffectEvent documentation for more examples.
cacheSignal (React Server Components)
cacheSignal is a new API for React Server Components that returns an AbortSignal tied to the cache lifetime. When the cache expires or the render is aborted, the signal triggers, allowing you to cancel in-flight requests.
Why It Matters
Without cacheSignal, fetch requests initiated during server rendering continue even if React no longer needs the result. This wastes bandwidth and server resources.
Basic Usage
import { cache, cacheSignal } from 'react';const fetchUserData = cache(async (userId: string) => { const signal = cacheSignal(); const response = await fetch(`/api/users/${userId}`, { signal }); if (!response.ok) { throw new Error('Failed to fetch user'); } return response.json();});When React's cache lifetime ends (the render completes or fails), the AbortSignal fires, canceling any pending fetch.
When to Use It
Use cacheSignal when:
- Making fetch requests in React Server Components
- Performing any async operation that supports
AbortSignal - You want to avoid wasted work when renders are interrupted
Integration with cache()
cacheSignal is designed to work with React's cache() function. The signal's lifetime matches the cache entry's lifetime:
import { cache, cacheSignal } from 'react';const getProducts = cache(async (categoryId: string) => { const signal = cacheSignal(); const [products, metadata] = await Promise.all([ fetch(`/api/products?category=${categoryId}`, { signal }).then(r => r.json()), fetch(`/api/categories/${categoryId}`, { signal }).then(r => r.json()), ]); return { products, metadata };});Both fetches cancel together if the cache is invalidated.
Production Gotcha: Handle AbortError
When a signal aborts, fetch throws an AbortError. If you don't handle it, you'll see unhandled promise rejections in your logs:
const fetchUserData = cache(async (userId: string) => { const signal = cacheSignal(); try { const response = await fetch(`/api/users/${userId}`, { signal }); return response.json(); } catch (error) { // Don't rethrow AbortError - the render was intentionally cancelled if (error instanceof Error && error.name === 'AbortError') { return null; } throw error; }});We hit this in Makerkit when renders were interrupted during navigation. The fix is straightforward: check for AbortError before rethrowing.
Partial Pre-rendering
Partial Pre-rendering (PPR) combines static and dynamic rendering in a single request. You pre-render a static shell to the CDN and stream dynamic content when the request arrives.
How It Works
- Build time: React pre-renders the static parts of your page (headers, navigation, product images)
- Request time: When a user requests the page, they immediately receive the static shell from the CDN
- Streaming: React resumes rendering the dynamic parts (personalized content, prices, stock levels) and streams them to the browser
Why It Matters
Traditional approaches force a choice:
- Static Generation: Fast but can't personalize
- Server Rendering: Personalized but slower TTFB
PPR gives you both: instant static content plus fresh dynamic data.
Use Cases
PPR works well for pages with clear static/dynamic splits:
- E-commerce product pages: Static product info, dynamic pricing and stock
- Dashboards: Static layout, dynamic data
- Content sites: Static article, dynamic comments and recommendations
Code Example
import { Suspense } from 'react';export default async function ProductPage({ params }) { // Static: fetched at build time const product = await getProduct(params.id); return ( <main> {/* Static shell - pre-rendered */} <h1>{product.name}</h1> <img src={product.image} alt={product.name} /> <p>{product.description}</p> {/* Dynamic - rendered at request time */} <Suspense fallback={<PriceSkeleton />}> <DynamicPrice productId={params.id} /> </Suspense> <Suspense fallback={<StockSkeleton />}> <StockLevel productId={params.id} /> </Suspense> </main> );}async function DynamicPrice({ productId }) { const pricing = await getPricing(productId); // Fresh on each request return <span className="price">${pricing.current}</span>;}The static content serves instantly from the CDN. The dynamic components stream in as they resolve.
Framework Support
PPR is React's underlying streaming capability that frameworks build upon. How you enable it depends on your framework:
- Next.js 15: PPR is available as an experimental
pprflag - Next.js 16: PPR evolved into Cache Components with explicit
"use cache"directives, giving you more granular control
For Next.js 16 users, see our Next.js 16 upgrade guide for details on Cache Components, which replace the experimental PPR flag with a more explicit caching model.
SSR Improvements
React 19.2 includes two significant improvements to server-side rendering.
Suspense Boundary Batching
Previously, during streaming SSR, each Suspense boundary revealed its content immediately when ready. This could cause jarring visual updates.
In React 19.2, Suspense boundaries batch their reveals for a short window. If multiple boundaries resolve close together, they reveal simultaneously, creating smoother loading experiences.
This aligns server behavior with client-side rendering, where React already batches updates.
Web Streams for Node
React 19.2 adds Web Streams support for Node.js environments:
renderToReadableStream: Improved streaming APIprerender: Pre-render to a stream for PPRresume: Resume a pre-rendered stream with dynamic content
These APIs enable PPR and improve streaming performance in Node-based deployments.
Chrome DevTools Performance Tracks
React 19.2 adds custom tracks to Chrome DevTools performance profiles, giving you visibility into React's internal scheduling.
Available Tracks
When you record a performance profile, you'll see:
- Scheduler Track: Shows what React is working on and at what priority
- Blocking: User interactions that need immediate response
- Transition: Updates inside
startTransitionthat can be deferred - Idle: Low-priority background work
How to Use Them
- Open Chrome DevTools
- Go to the Performance tab
- Record a profile while interacting with your app
- Look for the React-specific tracks in the flame chart
These tracks help you understand:
- Why certain updates feel slow (blocked by other work?)
- Whether transitions are being used effectively
- If React is scheduling work at appropriate priorities
Practical Debugging
If your app feels sluggish during navigation:
- Record a performance profile
- Check if navigation updates show as "Blocking" instead of "Transition"
- Wrap navigation state updates in
startTransitionif appropriate
ESLint Plugin v7
The latest ESLint plugins for React introduce stricter rules about hooks usage. These rules prepare your code for the React Compiler and catch common bugs.
Makerkit uses strict ESLint rules, so we've already adapted to these changes. Here's what you need to know.
Setting State in useEffect
This common pattern is now flagged:
function MyComponent({ value }) { const [state, setState] = useState(value); // ❌ Not allowed useEffect(() => { setState(value); }, [value]);}The fix is to update state during render:
function MyComponent({ value }) { const [state, setState] = useState(value); // ✅ Allowed if (value !== state) { setState(value); } return <div>{state}</div>;}This pattern is safe because React will re-render immediately with the new state, before committing to the DOM.
For more patterns, see You Might Not Need an Effect.
Scenario: useFetcher in React Router
useFetcher doesn't return a Promise, making it tempting to use useEffect for handling completion:
function MyComponent() { const fetcher = useFetcher(); const [isSuccess, setIsSuccess] = useState(false); // ❌ Setting state in effect useEffect(() => { if (fetcher.data?.success) { setIsSuccess(true); } }, [fetcher.data]);}The fix is to derive state directly:
function MyComponent() { const fetcher = useFetcher(); const isSuccess = fetcher.data?.success; const isError = fetcher.data && !fetcher.data.success; return ( <div> {isSuccess && <div>Success</div>} {isError && <div>Error</div>} <fetcher.Form method="post"> <input type="text" name="name" /> <button type="submit">Submit</button> </fetcher.Form> </div> );}For side effects like toasts, split into a form component with callbacks:
function MyForm({ onSuccess, onError }) { const fetcher = useFetcher(); useEffect(() => { if (fetcher.data) { fetcher.data.success ? onSuccess?.() : onError?.(); } }, [fetcher.data, onSuccess, onError]); return ( <fetcher.Form method="post"> <input type="text" name="name" /> <button type="submit">Submit</button> </fetcher.Form> );}function MyContainer() { const handleSuccess = useCallback(() => toast.success('Success'), []); const handleError = useCallback(() => toast.error('Error'), []); return <MyForm onSuccess={handleSuccess} onError={handleError} />;}Reading Refs in Render
Reading or writing ref.current during render is flagged:
function MyComponent() { const ref = useRef(0); // ❌ Not allowed return <div>{ref.current}</div>;}If the value needs to appear on screen, use useState:
function MyComponent() { const [count, setCount] = useState(0); return <div>{count}</div>;}Refs are for values that:
- Persist across renders without triggering re-renders
- Don't need to appear in the UI
- Are only read/written in effects or event handlers
Disabling Rules
If needed, disable per-line:
// eslint-disable-next-line react-hooks/set-state-in-effectOr globally in your ESLint config:
// tooling/eslint/base.jsrules: { 'react-hooks/set-state-in-effect': 'off',},I recommend fixing the code rather than disabling rules. These patterns prepare your codebase for the React Compiler.
Conclusion
React 19.2 refines how React handles effects, state, and visibility. The new APIs (<Activity>, useEffectEvent, cacheSignal, Partial Pre-rendering) move React toward a more declarative model. The stricter ESLint rules enforce patterns that make code more predictable and Compiler-ready.
Most importantly: If you're running React 19.0.0 through 19.2.2, patch immediately. CVE-2025-55182 is a critical RCE vulnerability that's being actively exploited. Update to 19.2.3 and rotate your secrets.
For existing codebases, I recommend:
- Patch first: Update to React 19.2.3 and your framework's patched version
- Audit effects: Use
useEffectEventto clean up unnecessary dependencies - Adopt Activity gradually: Start with tabs or modals where state preservation helps
- Enable strict ESLint rules: Prepare for the React Compiler
All Makerkit SaaS Starter Kits run on React 19.2.3 with all security patches applied:
We've migrated to the new patterns and can confirm they work well in production. If you're using Makerkit, make sure you're on the latest version to benefit from these security fixes.
Frequently Asked Questions
Is my React app vulnerable to React2Shell?
Can I use a WAF to protect against CVE-2025-55182?
What's the difference between Activity and conditional rendering?
When should I use useEffectEvent vs regular functions?
What is cacheSignal used for?
Do I need to update if I only use client-side React?
Resources
React Documentation:
- React 19.2 Release Announcement
- Critical Security Vulnerability in React Server Components
- DoS and Source Code Exposure Advisory
- Activity Component Reference
- useEffectEvent Reference
- You Might Not Need an Effect
- React Compiler
Related MakerKit Guides:
- Next.js 16 Upgrade Guide - Turbopack, Cache Components, and more
- Server Actions Guide - Complete guide to Server Actions patterns
- Secure Server Actions - Security best practices for Server Actions