Back to topics

Learning React 19.2 New Features

This article "Feynman-ifies" some new features of React 19.2.

<Activity /> – React's Take on Keep-Alive

<Activity /> is a new built-in component that helps us control the behavior of invisible parts (e.g., hidden tab content in a Tabs component).

The experience is similar to Vue's v-show – we can use it to hide components we don't need to render.

Hidden components:

  • Clean up effects (like event listeners, persistent timers) via cleanup to reduce unnecessary performance overhead.
  • Maintain low-priority activity, silently updating the UI during idle browser time.
  • When made visible, have lower rendering cost and better performance (better than conditional rendering like condition && or display: none).

This means smoother tab switching – no need to re-fetch data or lose scroll position.

Compared to conditional rendering:

{activeTab === 'reports' && <Reports />}
{activeTab === 'settings' && <Settings />}

When switching tabs:

  • React unmounts the old component and mounts the new one.
  • The old component's state is lost.
  • Effects re-run.

With Activity, there's no frequent unmounting/mounting, no need to manually reset state, and effects are paused.

Compared to display: none, both hide components, but Activity makes background effects more controllable.

Another use case is pre-rendering. With conditional rendering, the component's logic only starts when the condition is true. With Activity, even when hidden, it can silently fetch data in the background, reducing user waiting time (though this feature is quite meta – you definitely shouldn't fetch data inside useEffect).

My verdict: it's long overdue. Keep-Alive has been around for years, and React is just now catching up.

useEffectEvent – Savior of the Effect Dependency Array

Shouldn't this have been considered when designing useEffect? Why did it take so long?

Take a simple example: after a WebSocket connection succeeds, print the user's name.

You'd have to put user.name in the dependency array.

useEffect(() => {
    const connection = createConnection();
    connection.on('message', (msg) => {
        console.log('New message for', user.name);
    });
    return() => connection.disconnect();
}, [user.name]);

That means any change to user.name (or something unrelated to WebSocket, like the page theme in the official docs) would cause the WebSocket to reconnect and the page to flicker.

Now we can use useEffectEvent to keep the effect stable.

const onMessage = useEffectEvent((msg) => {
    console.log('New message for', user.name);
});

useEffect(() => {
    const connection = createConnection();
    connection.on('message', onMessage);
    return() => connection.disconnect();
}, []);

Now:

  • The effect runs only once.
  • onMessage always gets the latest user.name.
  • ESLint warnings are reduced.

cacheSignal() – Slowly Becoming Like Next.js

This feature is designed for server components.

Simply put, it tells the user when the lifecycle of cache() ends, so the user can cancel unnecessary operations (e.g., stale data fetches).

Previously, we could use cache to memoize fetch results.

import { cache } from'react';

const fetchPostMeta = cache(async (postId) => {
    console.log(`Fetching metadata for post ${postId}`);
    const res = awaitfetch(`https://api.example.com/posts/${postId}/meta`);
    return res.json();
});

asyncfunctionPost({ postId }) {
const meta = awaitfetchPostMeta(postId);
return (
<div>
    <h1>Post {postId}</h1>
    <p>Views: {meta.views}</p>
</div>
  );
}

If the user leaves the page before the fetch completes, the API request still runs in the background.

This wastes server resources. Now we can pass a signal to the fetch, allowing it to automatically cancel stale requests when the cache is invalidated.

import { cache, cacheSignal } from'react';

const fetchPostMeta = cache(async (postId, { signal }) => {
console.log(`Fetching metadata for post ${postId}`);
const res = awaitfetch(`https://api.example.com/posts/${postId}/meta`, { signal });
return res.json();
});

asyncfunctionPost({ postId }) {
    const signal = cacheSignal();
    const meta = awaitfetchPostMeta(postId, { signal });
    return (
    <div>
        <h1>Post {postId}</h1>
        <p>Views: {meta.views}</p>
    </div>
    );
}

Others

There are also some very meta features like Partial Pre-rendering, batching Suspense, etc.

Obviously, these features aren't meant for ordinary users; they're mainly intended to support frameworks like Next.js.

Partial Pre-rendering (PPR): In simple terms, static parts of a page (like header/footer) can now be pre-rendered and cached, while dynamic parts can be resumed later.

const result = awaitprerender(<Page />);
const html = renderToString(result.prelude);

React first renders the static part (result.prelude), sends it to the browser, then resumes the dynamic part once the data is ready. Users see content immediately, and React seamlessly fills in the rest later.

Batching Suspense Boundaries: Also designed for SSR.

Previously, Suspense parts rendered at different times, causing page jitter. Now it waits a short while to batch multiple Suspense boundaries together, making changes appear more natural.