Back to All Articles
reactperformancefrontendssrtypescript

Mastering React 19.2: Practical Guide to Activity, useEffectEvent, and Partial Pre-rendering

Learn how to optimize React apps using React 19.2 features like <Activity>, useEffectEvent, and partial pre-rendering for better performance.

2026-10-29
Mastering React 19.2: Practical Guide to Activity, useEffectEvent, and Partial Pre-rendering

Let's look at how to leverage React 19.2's latest primitives to solve tricky UI state retention, clean up chat connection effects, and ship faster initial payloads via partial pre-rendering.

What you'll learn:

  • Managing hidden view state efficiently with the new component.
  • Isolating non-reactive event logic from effects using useEffectEvent.
  • Setting up partial pre-rendering and stream resumption for faster page loads.
  • Avoiding common performance traps with new React DevTools scheduler tracks.

Keeping Component State Alive with

Conditionally rendering heavy components via boolean flags like {isVisible && } unmounts the tree, destroying local state and form inputs. React 19.2 introduces to hide parts of your app while preserving internal state and running effects only when visible.

TSX
import { Activity } from 'react';
import { useState } from 'react';

export function DashboardShell() {
  const [currentTab, setCurrentTab] = useState<'feed' | 'settings'>('feed');

  return (
    <div className="flex flex-col h-screen">
      <nav className="flex gap-4 p-4 border-b">
        <button onClick={() => setCurrentTab('feed')}>Feed</button>
        <button onClick={() => setCurrentTab('settings')}>Settings</button>
      </nav>

      {/* Feed stays mounted and preserves scroll position / inputs */}
      <Activity mode={currentTab === 'feed' ? 'visible' : 'hidden'}>
        <HeavyFeedComponent />
      </Activity>

      <Activity mode={currentTab === 'settings' ? 'visible' : 'hidden'}>
        <SettingsComponent />
      </Activity>
    </div>
  );
}

Speed Note: Hidden activities defer all non-urgent updates until React has idle time, keeping your active view snappy and responsive.

Isolating Non-Reactive Logic with useEffectEvent

When building effects that sync with external systems, referencing dynamic props like a theme string often forces unwanted reconnections. useEffectEvent lets us extract non-reactive event handlers out of the effect dependency array.

TSX
import { useState, useEffect, useEffectEvent } from 'react';

export function ChatRoom({ roomId, theme }: { roomId: string; theme: string }) {
  // This hook always captures the latest theme without triggering effect re-runs
  const onConnected = useEffectEvent(() => {
    showNotification('Successfully joined chat', theme);
  });

  useEffect(() => {
    const connection = createConnection(roomId);

    connection.on('connected', () => {
      onConnected();
    });

    connection.connect();

    return () => {
      connection.disconnect();
    };
  }, [roomId]); // ✅ theme is safely excluded without breaking linter rules

  return <div className={theme}>Active Room: {roomId}</div>;
}

Gotcha: Only use useEffectEvent for functions that represent events triggered inside an effect. Don't use it as a generic shortcut to silence dependency linter warnings.

Pre-rendering Static Shells with Server Streams

React 19.2 adds partial pre-rendering capabilities, letting you generate static shells ahead of time and resume streaming dynamic content later on the server.

TSX
import { prerender } from 'react-dom/server';

export async function handleServerRequest(request: Request) {
  const controller = new AbortController();

  // Generate static prelude shell
  const { prelude, postponed } = await prerender(<AppRoot />, {
    signal: controller.signal,
  });

  // Save postponed state for later hydration/resumption
  await persistPostponedState(request.id, postponed);

  return new Response(prelude, {
    headers: { 'Content-Type': 'text/html' },
  });
}

Accessibility Note: When resuming streams, keep your markup semantic. Ensure batched Suspense boundaries don't delay critical landmark regions for screen readers.

Profiling Workloads with Chrome DevTools Tracks

Optimizing complex React trees requires knowing when scheduler priorities block main-thread rendering. React 19.2 integrates custom tracks directly into Chrome DevTools performance profiles.

TSX
// Example of wrapping expensive transitions to inspect scheduler chunks
import { startTransition, useState } from 'react';

export function DataViewer({ data }: { data: any[] }) {
  const [filter, setFilter] = useState('');

  const handleFilterChange = (value: string) => {
    startTransition(() => {
      // Visible in the "Scheduler ⚛" track under transition priority
      setFilter(value);
    });
  };

  return <input onChange={(e) => handleFilterChange(e.target.value)} />;
}

Tip: Open Chrome DevTools, record a profile, and inspect the new Scheduler ⚛ and Components ⚛ tracks to pinpoint exact mount bottlenecks and blocked render intervals.

Summary

React 19.2 gives us granular control over component lifecycles, effect dependencies, and streaming payloads. By adopting for state retention and useEffectEvent for clean external system syncs, your component code stays robust and maintainable.

Next Steps:

  • Upgrade your linter configuration to eslint-plugin-react-hooks@latest to support flat config and new hook validations.
  • Audit your heavy tab navigations and replace conditional unmounting with .
  • Profile a heavy rendering path using the updated Chrome DevTools performance tracks.