# 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. - Published: 2026-06-03 - Updated: 2026-06-03 - Author: SharpSkill - Tags: react, useEffectEvent, activity, react 19, hooks, interview - Reading time: 11 min --- React 19.2 introduced two APIs that address long-standing pain points: `useEffectEvent` eliminates stale closures in effects, and `` enables background pre-rendering with state preservation. Both shipped in [October 2025](https://react.dev/blog/2025/10/01/react-19-2) and are already changing how production React applications handle side effects and navigation. > **React 19.2 Minimum** > > 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: ```tsx // hooks/useChatRoom.ts // 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. ```tsx // hooks/useChatRoom.ts // 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](https://react.dev/reference/react/useEffectEvent) 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`: 1. Call it at the top level of a component or custom hook — never inside loops or conditions 2. Only call the returned function from inside `useEffect` or another Effect Event 3. 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](https://react.dev/reference/react/useEffectEvent) automatically. ```tsx // components/SearchTracker.tsx // 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([]) // 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 } ``` > **When Not to Use useEffectEvent** > > 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 `` 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, `` preserves state while cleaning up effects and deferring updates. ```tsx // components/TabLayout.tsx // 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 (
{tabs.map((tab) => ( {tab.content} ))}
) } ``` 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. ## Activity Modes: Visible vs Hidden Under the Hood The `` 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. ```tsx // components/PrerenderedRoute.tsx // Pre-render the next likely route in the background import { Activity, Suspense, use } from 'react' interface PrerenderedRouteProps { isActive: boolean dataPromise: Promise } export function PrerenderedRoute({ isActive, dataPromise }: PrerenderedRouteProps) { return ( }> ) } function DashboardContent({ dataPromise }: { dataPromise: Promise }) { // use() reads the cached promise — works during hidden pre-render const data = use(dataPromise) return (
) } ``` The hidden `` 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 `` involves [TanStack Query](https://tanstack.com/query/latest). Since `useQuery` relies on `useEffect` internally, queries inside a hidden `` will not execute — the effect is unmounted. ```tsx // components/UserDashboard.tsx // 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 } // 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 ( ) } ``` The fix is straightforward: move data prefetching to a parent component or use `queryClient.ensureQueryData` outside the `` boundary. The cached data will be available when the hidden component becomes visible. > **Memory Trade-off** > > 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. ```tsx // components/LiveDashboard.tsx import { useEffect, useEffectEvent, useState } from 'react' import { Activity } from 'react' function LiveFeed({ channel, userId }: { channel: string; userId: string }) { const [messages, setMessages] = useState([]) // 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 } export function LiveDashboard({ userId }: { userId: string }) { const [activeChannel, setActiveChannel] = useState('general') const channels = ['general', 'alerts', 'metrics'] return (
{channels.map((ch) => ( ))}
) } ``` When switching channels, the hidden feed's WebSocket disconnects (effect cleanup via ``). 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](/technologies/react-next/interview-questions/react-hooks) 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](https://react.dev/reference/react/useEffectEvent). **Q3: How does Activity differ from conditional rendering and CSS display:none?** Conditional rendering (`{show && }`) unmounts the component entirely — state is destroyed. CSS `display: none` hides visually but keeps all effects running, wasting resources. `` 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 `` 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](/technologies/react-next/interview-questions/react-query-tanstack) 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 `` with a `` 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](https://react.dev/reference/react/useEffect) 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 - `useEffectEvent` replaces the `useRef` workaround for stale closures in effects, with native linter support in `eslint-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 - `` 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 `Suspense` and the `use` API - [TanStack Query](/technologies/react-next/interview-questions/react-query-tanstack) and other effect-based libraries need prefetching outside Activity boundaries since `useEffect` does not run in hidden mode - Both APIs ship in React 19.2 — update ESLint and React together for full tooling support --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/en/blog/react-next/react-19-useeffectevent-activity-new-apis-interview-questions