React 19 useEffectEvent and Activity: New APIs and Interview Questions 2026
Deep dive into React 19.2 useEffectEvent and Activity component. Stale closure solutions, background pre-rendering, code examples, and interview questions.

React 19.2 introduced two APIs that address long-standing pain points: useEffectEvent eliminates stale closures in effects, and <Activity> enables background pre-rendering with state preservation. Both shipped in October 2025 and are already changing how production React applications handle side effects and navigation.
useEffectEvent and Activity require React 19.2 or later. Update with npm install react@latest react-dom@latest. The ESLint plugin eslint-plugin-react-hooks@6+ adds native support for useEffectEvent in dependency arrays.
What useEffectEvent Solves: The Stale Closure Problem
Every React developer has encountered stale closures. An effect captures a value at render time, and when that value changes, the effect still references the old one. The classic workaround involved useRef to hold a mutable reference — functional but verbose and invisible to the linter.
Consider a chat application that logs analytics when a new message arrives. The log should include the current theme, but theme changes should not reconnect the chat:
// Before useEffectEvent: useRef workaround
import { useEffect, useRef } from 'react'
export function useChatRoom(roomId: string, theme: string) {
// Store theme in a ref to avoid stale closure
const themeRef = useRef(theme)
themeRef.current = theme
useEffect(() => {
const connection = createConnection(roomId)
connection.on('message', (msg) => {
// themeRef.current always has the latest value
logAnalytics('new_message', { roomId, theme: themeRef.current })
showNotification(msg)
})
connection.connect()
return () => connection.disconnect()
}, [roomId]) // theme intentionally excluded — but linter warns
}This works, but the linter flags theme as a missing dependency. Suppressing the warning hides potential bugs elsewhere. The useRef dance also obscures intent: a new developer reading this code has to mentally trace why theme sits in a ref instead of the dependency array.
useEffectEvent: Separating Reactive from Non-Reactive Logic
The useEffectEvent hook creates a stable function that always reads the latest props and state, without triggering effect re-synchronization. It replaces the useRef pattern with a declarative API that the linter understands natively.
// After useEffectEvent: clean separation of concerns
import { useEffect, useEffectEvent } from 'react'
export function useChatRoom(roomId: string, theme: string) {
// Effect Event: always reads latest theme, never triggers reconnect
const onMessage = useEffectEvent((msg: string) => {
logAnalytics('new_message', { roomId, theme })
showNotification(msg)
})
useEffect(() => {
const connection = createConnection(roomId)
connection.on('message', onMessage)
connection.connect()
return () => connection.disconnect()
}, [roomId]) // No linter warning — onMessage is an Effect Event
}The effect only re-runs when roomId changes. The onMessage callback always sees the current theme without being listed as a dependency. The official React documentation explicitly states that Effect Events must only be called from inside effects — never during rendering or passed to child components.
useEffectEvent Rules and Constraints
Three rules govern useEffectEvent:
- Call it at the top level of a component or custom hook — never inside loops or conditions
- Only call the returned function from inside
useEffector another Effect Event - Never pass it as a prop or return it from a hook for external consumption
Breaking rule 2 causes subtle bugs: the function identity changes every render, so storing it in a ref or passing it down defeats the purpose. The eslint-plugin-react-hooks@6+ enforces these constraints automatically.
// Correct: Effect Event used inside useEffect
import { useEffect, useEffectEvent, useState } from 'react'
interface SearchTrackerProps {
query: string
userId: string
}
export function SearchTracker({ query, userId }: SearchTrackerProps) {
const [results, setResults] = useState<string[]>([])
// Track searches with current user context
const onSearchComplete = useEffectEvent((resultCount: number) => {
analytics.track('search_complete', {
query,
userId, // Always latest userId
resultCount,
timestamp: Date.now(),
})
})
useEffect(() => {
const controller = new AbortController()
fetchSearchResults(query, controller.signal).then((data) => {
setResults(data)
onSearchComplete(data.length) // Called from inside useEffect
})
return () => controller.abort()
}, [query]) // userId excluded safely — lives in the Effect Event
return <ResultsList results={results} />
}useEffectEvent is not an escape hatch to silence the dependency linter. If a value genuinely controls when an effect should re-run, it belongs in the dependency array. Only extract logic into an Effect Event when it represents a side action (logging, notification, analytics) that reads reactive values without needing to re-trigger the effect.
The Activity Component: Background Pre-Rendering with State Preservation
The <Activity> component (formerly called "Offscreen") controls whether its children are visible or hidden. Unlike conditional rendering which destroys state, or CSS display: none which keeps effects running, <Activity> preserves state while cleaning up effects and deferring updates.
// Activity preserves form state across tab switches
import { useState } from 'react'
import { Activity } from 'react'
interface Tab {
id: string
label: string
content: React.ReactNode
}
export function TabLayout({ tabs }: { tabs: Tab[] }) {
const [activeTab, setActiveTab] = useState(tabs[0].id)
return (
<div>
<nav className="flex gap-2 border-b border-border">
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={activeTab === tab.id ? 'border-b-2 border-primary' : ''}
>
{tab.label}
</button>
))}
</nav>
{tabs.map((tab) => (
<Activity key={tab.id} mode={activeTab === tab.id ? 'visible' : 'hidden'}>
{tab.content}
</Activity>
))}
</div>
)
}A user filling out a form in the "Profile" tab can switch to "Settings" and back without losing input. The hidden tab's effects (timers, subscriptions, data fetching) are cleaned up, freeing resources. When the tab becomes visible again, effects remount and state is restored instantly.
Ready to ace your React / Next.js interviews?
Practice with our interactive simulators, flashcards, and technical tests.
Activity Modes: Visible vs Hidden Under the Hood
The <Activity> component accepts a mode prop with two values:
| Behavior | mode="visible" | mode="hidden" |
|----------|-------------------|------------------|
| DOM rendering | Normal | display: none via CSS |
| Component state | Active | Preserved in memory |
| Effects (useEffect) | Mounted | Cleaned up |
| Update priority | Normal | Deferred to idle |
| Pre-rendering | N/A | Renders at low priority |
When a component starts hidden (initial render with mode="hidden"), React pre-renders it at low priority without mounting effects. This enables instant navigation: the target page is already rendered in the background when the user clicks.
// Pre-render the next likely route in the background
import { Activity, Suspense, use } from 'react'
interface PrerenderedRouteProps {
isActive: boolean
dataPromise: Promise<DashboardData>
}
export function PrerenderedRoute({ isActive, dataPromise }: PrerenderedRouteProps) {
return (
<Activity mode={isActive ? 'visible' : 'hidden'}>
<Suspense fallback={<DashboardSkeleton />}>
<DashboardContent dataPromise={dataPromise} />
</Suspense>
</Activity>
)
}
function DashboardContent({ dataPromise }: { dataPromise: Promise<DashboardData> }) {
// use() reads the cached promise — works during hidden pre-render
const data = use(dataPromise)
return (
<div className="grid grid-cols-3 gap-4">
<MetricsCard data={data.metrics} />
<ChartPanel data={data.charts} />
<RecentActivity items={data.activity} />
</div>
)
}The hidden <Activity> pre-renders the dashboard at idle priority. Combined with Suspense and the use API, data fetching happens in the background. When isActive flips to true, the content appears without any loading spinner.
Activity and TanStack Query: The Cache Gotcha
A common pitfall with <Activity> involves TanStack Query. Since useQuery relies on useEffect internally, queries inside a hidden <Activity> will not execute — the effect is unmounted.
// Problem: useQuery won't fetch when Activity is hidden
import { useQuery, useQueryClient } from '@tanstack/react-query'
import { Activity } from 'react'
function UserStats() {
// This useQuery will NOT run while hidden
const { data } = useQuery({
queryKey: ['user-stats'],
queryFn: fetchUserStats,
})
return <StatsDisplay data={data} />
}
// Solution: prefetch data outside the Activity boundary
function DashboardWithPrefetch({ showStats }: { showStats: boolean }) {
const queryClient = useQueryClient()
// Prefetch at the parent level — runs regardless of Activity mode
queryClient.ensureQueryData({
queryKey: ['user-stats'],
queryFn: fetchUserStats,
})
return (
<Activity mode={showStats ? 'visible' : 'hidden'}>
<UserStats />
</Activity>
)
}The fix is straightforward: move data prefetching to a parent component or use queryClient.ensureQueryData outside the <Activity> boundary. The cached data will be available when the hidden component becomes visible.
Activity trades memory for speed. Each hidden component tree stays in memory with its full DOM. For applications with many hidden routes, monitor memory usage. The React team is exploring automatic eviction of least-recently-used hidden Activities in future releases.
Combining useEffectEvent and Activity
Both APIs complement each other in real-world navigation patterns. A common scenario: a tabbed dashboard where each tab has WebSocket subscriptions and analytics tracking.
import { useEffect, useEffectEvent, useState } from 'react'
import { Activity } from 'react'
function LiveFeed({ channel, userId }: { channel: string; userId: string }) {
const [messages, setMessages] = useState<Message[]>([])
// Analytics tracking with latest userId — no effect re-sync
const onNewMessage = useEffectEvent((msg: Message) => {
analytics.track('live_message', { channel, userId })
setMessages((prev) => [...prev, msg])
})
useEffect(() => {
const ws = new WebSocket(`wss://api.example.com/${channel}`)
ws.onmessage = (event) => {
const msg = JSON.parse(event.data) as Message
onNewMessage(msg)
}
return () => ws.close()
}, [channel]) // Clean reconnect only when channel changes
return <MessageList messages={messages} />
}
export function LiveDashboard({ userId }: { userId: string }) {
const [activeChannel, setActiveChannel] = useState('general')
const channels = ['general', 'alerts', 'metrics']
return (
<div>
<nav className="flex gap-2">
{channels.map((ch) => (
<button key={ch} onClick={() => setActiveChannel(ch)}>
{ch}
</button>
))}
</nav>
{channels.map((ch) => (
<Activity key={ch} mode={activeChannel === ch ? 'visible' : 'hidden'}>
<LiveFeed channel={ch} userId={userId} />
</Activity>
))}
</div>
)
}When switching channels, the hidden feed's WebSocket disconnects (effect cleanup via <Activity>). The message history survives in state. The onNewMessage Effect Event ensures analytics always reference the current userId without forcing WebSocket reconnections.
Interview Questions: useEffectEvent and Activity
These questions test understanding of the new APIs and their interaction with React's rendering model. They are increasingly common in React interviews at companies using React 19.2.
Q1: What problem does useEffectEvent solve that useCallback cannot?
useCallback creates a memoized function, but it still needs to be listed in the effect's dependency array. If any of its own dependencies change, the callback changes, which re-triggers the effect. useEffectEvent creates a function that always reads the latest values without being a dependency — the effect never re-runs because of it. This separation is impossible with useCallback alone.
Q2: Can an Effect Event be passed as a prop to a child component?
No. Effect Events are designed to be called only from inside useEffect or other Effect Events. Their identity changes every render, so passing them as props would cause unnecessary re-renders and break the mental model. The ESLint plugin enforces this rule.
Q3: How does Activity differ from conditional rendering and CSS display:none?
Conditional rendering ({show && <Component />}) unmounts the component entirely — state is destroyed. CSS display: none hides visually but keeps all effects running, wasting resources. <Activity mode="hidden"> preserves state, cleans up effects, defers updates to idle priority, and can pre-render content in the background.
Q4: What happens to useEffect inside a hidden Activity?
When an <Activity> transitions to mode="hidden", React runs all effect cleanup functions (the return value of useEffect). No new effects mount while hidden. When the component becomes visible again, effects remount with the preserved state. This is why data fetching libraries that rely on useEffect need prefetching strategies outside the Activity boundary.
Q5: How would you pre-render a route with Activity and Suspense?
Wrap the route in <Activity mode="hidden"> with a <Suspense> boundary inside. Use the use() API or a Suspense-enabled data source for fetching. React renders the hidden tree at low priority, resolving the Suspense boundary in the background. When the user navigates and mode flips to "visible", the fully rendered content appears instantly without a loading state.
Q6: Is useEffectEvent a replacement for the exhaustive-deps lint rule?
No. The exhaustive-deps rule remains critical for catching genuine missing dependencies. useEffectEvent handles a specific case: logic that reads reactive values but should not control when the effect re-runs (analytics, notifications, logging). Using it to suppress all dependency warnings hides bugs and defeats its purpose.
Conclusion
useEffectEventreplaces theuseRefworkaround for stale closures in effects, with native linter support ineslint-plugin-react-hooks@6+- Effect Events always read the latest props and state without triggering effect re-synchronization — use them for analytics, logging, and notification callbacks
<Activity>preserves component state while cleaning up effects, offering a middle ground between conditional rendering and CSS hiding- Hidden Activities pre-render at idle priority, enabling instant navigation when combined with
Suspenseand theuseAPI - TanStack Query and other effect-based libraries need prefetching outside Activity boundaries since
useEffectdoes not run in hidden mode - Both APIs ship in React 19.2 — update ESLint and React together for full tooling support
Start practicing!
Test your knowledge with our interview simulators and technical tests.
Tags
Share
Related articles

React Compiler in 2026: Automatic Memoization and Interview Questions
Master React Compiler interview questions for 2026. Covers automatic memoization, HIR compilation pipeline, Rules of React, ESLint integration, and when manual optimization still matters.

Next.js 16 Cache Components in 2026: use cache, PPR and Interview Questions
Deep dive into Next.js 16 Cache Components: the use cache directive, Partial Pre-Rendering (PPR), cacheLife, cacheTag, and real interview questions for senior developers.

React Server Components in Production: Patterns and Pitfalls
React Server Components in production: battle-tested patterns, common anti-patterns, and debugging strategies for robust Next.js 15 applications.