Skip to content
chornous.dev

Type two or more letters. Esc closes.

Index of sheets
Theme

Sheet 03 · Writing

All notes

React 19.2: Activity, useEffectEvent, performance tracks and partial pre-rendering

React 19.2 shipped on October 1, 2025 with Activity for hidden UI that keeps state, useEffectEvent for effect logic that reads fresh values, Chrome DevTools performance tracks, partial pre-rendering and a new useId prefix.

Pl. 65 · tree drawing generated from the slug “react-19-2-activity-effect-event-notes”

The React team published React 19.2 to npm on October 1, 2025. It is the third release in a year, after React 19 in December and 19.1 in June. Two APIs give names to patterns you were already faking: <Activity> hides UI and keeps its state, and useEffectEvent lets an effect read fresh values without re-running. The release also adds Chrome DevTools tracks, partial pre-rendering and a change to Suspense streaming that affects your LCP.

Activity: hide a tree, keep its state

<Activity> replaces the && pattern you use to show and hide parts of a page:

// Before: unmounting throws away state
{isVisible && <Page />}

// After: hidden keeps state and pauses the work
<Activity mode={isVisible ? "visible" : "hidden"}>
  <Page />
</Activity>

In hidden mode React hides the children, unmounts their effects and defers their updates until it has nothing else to do. Switch back to visible and React remounts the effects with the old state intact.

You get three things out of it. A tab panel keeps its scroll position and half-typed form. A back navigation restores input fields. And you can render the page a user will likely open next while they read the current one, loading its data, CSS and images at low priority. The team says more modes will follow.

Hidden trees still cost memory, so wrap the screens people return to and leave the rest alone.

useEffectEvent ends the eslint-disable comment

The classic bug: a chat room effect that shows a notification with the current theme. Add theme to the dependencies and a theme change reconnects the socket. Leave it out and you disable the lint rule, then ship a stale value six months later.

function ChatRoom({ roomId, theme }) {
  const onConnected = useEffectEvent(() => {
    showNotification("Connected!", theme);
  });

  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.on("connected", () => onConnected());
    connection.connect();
    return () => connection.disconnect();
  }, [roomId]); // onConnected stays out of the array
}

An Effect Event reads the latest props and state, like a DOM event handler. Keep it out of the dependency array, and declare it in the same component or Hook as its effect. The linter checks both. The team warns against using it to silence the linter. Use it for logic that the effect fires, such as analytics, notifications and logging, and keep values that should restart the effect as dependencies.

To get the rule right, update eslint-plugin-react-hooks to v6. Its recommended preset now uses flat config, and new React Compiler rules are opt-in. If you still use legacy config:

- extends: ["plugin:react-hooks/recommended"]
+ extends: ["plugin:react-hooks/recommended-legacy"]

Performance tracks in Chrome DevTools

React now writes its own tracks into Chrome performance profiles. The Scheduler track shows work by priority, blocking for interactions and transition for startTransition, plus the event that scheduled each update and the moment React waited for paint. The Components track shows which components rendered or ran effects, with labels like Mount and Blocked.

Before, you guessed from flame charts full of anonymous functions. Now you record a profile, find the slow interaction and see the component that caused it on the same timeline.

Partial pre-rendering

React DOM can now pre-render the static shell of a page, serve it from a CDN, and resume rendering the dynamic parts later:

const { prelude, postponed } = await prerender(<App />, {
  signal: controller.signal,
});
await savePostponedState(postponed);

// Later, per request:
const stream = await resume(<App />, await getPostponedState(request));

resume and resumeAndPrerender cover Web Streams, and resumeToPipeableStream and resumeAndPrerenderToNodeStream cover Node streams. Frameworks will use these more than apps will. Next.js builds its own partial pre-rendering on the same idea.

Changes that alter behavior

React 19.2 changes to check after upgrading
ChangeWhat to check
SSR batches Suspense reveals for a short time, and stops batching near the 2.5 s LCP markStreaming pages reveal content in bigger chunks
useId prefix changes from «r» to rSnapshot tests and CSS that match generated IDs
Web Streams APIs work in Node.jsKeep Node streams, which the team calls faster and which compress by default
Context stringifies as SomeContext, not SomeContext.ProviderTests that assert on component names

React changed the useId prefix so IDs work as view-transition-name values and XML names. If your snapshot tests store «r1», they fail on upgrade. The Suspense batching brings server streaming in line with client rendering and prepares for <ViewTransition>.

Server Components also gain cacheSignal(), an AbortSignal that fires when the cache() lifetime ends, so you can abort a fetch React no longer needs.

This week

  1. Upgrade react, react-dom and eslint-plugin-react-hooks together. Re-run snapshot tests and update the useId prefixes.
  2. Grep for eslint-disable-next-line react-hooks/exhaustive-deps. Each hit is a candidate for useEffectEvent.
  3. Pick one tab or panel that loses state on switch and wrap it in <Activity>.
  4. Record a Chrome profile of your slowest interaction and read the Scheduler and Components tracks before you start optimizing.

Volodymyr Chornous