XplormityXplormity
HomeHandbooks
Browse
Xplormity

TLDR developer handbooks for
seasoned developers.

Handbooks

RustNestJSNext.jsGitDockerTypeScriptReactNode.jsDSASQLSystem DesignTailwind CSS

Site

HomeHandbooksAboutPrivacyTerms

Connect

GitHubTwitterLinkedIn

© 2026 Xplormity. All rights reserved.

HandbooksReactReact 19 — New Features & Patterns

React 19 — New Features & Patterns

react-19compileractionsuseOptimistichandbook

TL;DR

  • React Compiler auto-memoizes — no more manual useMemo/useCallback in most cases.
  • Actions (useActionState, useTransition with async) replace boilerplate form handling.
  • useOptimistic — instant UI updates while server processes.
  • use() hook reads Promises & Context (can be called conditionally!).
  • Activity component — keep hidden subtrees alive without re-rendering.
  • ref as prop — no more forwardRef.

Step 1: React Compiler (Auto-Memoization)

The React team spent 3+ years building the Compiler because manual memoization was the #1 source of developer frustration. Teams were wrapping everything in useMemo, useCallback, and React.memo defensively, creating code that was harder to read than the performance problem it solved. The Compiler analyzes your code at build time, understands React's rules, and inserts memoization only where it actually helps — automatically. It's the reason React 19 lets you "just write code" without thinking about re-render optimization.

The Compiler is a Babel plugin that automatically inserts memoization at build time:

// Before (React 18) — manual memoization everywhere
const ExpensiveComponent = memo(({ data, onClick }) => {
  const processed = useMemo(() => processData(data), [data]);
  const handleClick = useCallback(() => onClick(processed), [processed, onClick]);
  return <div onClick={handleClick}>{processed.title}</div>;
});

// After (React 19 + Compiler) — just write normal code
function ExpensiveComponent({ data, onClick }) {
  const processed = processData(data);
  const handleClick = () => onClick(processed);
  return <div onClick={handleClick}>{processed.title}</div>;
}
// Compiler automatically determines what to memoize ✅

Enabling the Compiler

npm install babel-plugin-react-compiler
// babel.config.js or next.config.ts
module.exports = {
  experimental: {
    reactCompiler: true, // Next.js 15+
  },
};

What the Compiler WON'T optimize

  • Components that violate Rules of React (mutating props, conditional hooks)
  • It tells you exactly which files it skipped and why

Step 2: useActionState — Form Handling

Every React form before React 19 required the same boilerplate: useState for loading, another for error, another for data, a submit handler that sets loading, calls the API, catches errors, and resets loading. useActionState was created to eliminate this repetitive pattern by combining state + action + loading into a single hook. It also integrates with progressive enhancement — forms work even before JavaScript loads, which matters for SSR-heavy apps.

Replaces the useState + loading + error boilerplate:

import { useActionState } from 'react';

// Action function: (previousState, formData) → newState
async function createPost(prevState: any, formData: FormData) {
  const title = formData.get('title') as string;

  if (!title) return { error: 'Title is required' };

  const response = await fetch('/api/posts', {
    method: 'POST',
    body: JSON.stringify({ title }),
    headers: { 'Content-Type': 'application/json' },
  });

  if (!response.ok) return { error: 'Failed to create post' };

  return { success: true, message: 'Post created!' };
}

function CreatePostForm() {
  const [state, formAction, isPending] = useActionState(createPost, null);
  //     ^state  ^pass to form  ^loading boolean

  return (
    <form action={formAction}>
      <input name="title" placeholder="Post title" />
      {state?.error && <p className="text-red-500">{state.error}</p>}
      {state?.success && <p className="text-green-500">{state.message}</p>}
      <button disabled={isPending}>
        {isPending ? 'Creating...' : 'Create Post'}
      </button>
    </form>
  );
}

Step 3: useOptimistic — Instant UI Updates

Users hate waiting. Before useOptimistic, implementing optimistic UI meant manually managing temporary state, rolling back on errors, and reconciling with server responses — each requiring 20+ lines of careful state logic. useOptimistic was built because every modern app (chat, likes, todo lists) needs instant feedback while the server processes in the background. It handles the temporary state and automatic reconciliation in one hook, making what was previously error-prone into a 3-line pattern.

Show the result immediately, reconcile when server responds:

import { useOptimistic } from 'react';

type Message = { id: string; text: string; pending?: boolean };

function Chat({ messages }: { messages: Message[] }) {
  const [optimisticMessages, addOptimistic] = useOptimistic(
    messages,
    (state, newMessage: Message) => [...state, { ...newMessage, pending: true }]
  );

  async function sendMessage(formData: FormData) {
    const text = formData.get('message') as string;
    const tempId = crypto.randomUUID();

    // Show immediately in UI (with pending flag)
    addOptimistic({ id: tempId, text });

    // Actually send to server
    await fetch('/api/messages', {
      method: 'POST',
      body: JSON.stringify({ text }),
    });
    // When server responds, React reconciles with real data
  }

  return (
    <div>
      {optimisticMessages.map(msg => (
        <div key={msg.id} style={{ opacity: msg.pending ? 0.5 : 1 }}>
          {msg.text} {msg.pending && '(sending...)'}
        </div>
      ))}
      <form action={sendMessage}>
        <input name="message" />
        <button type="submit">Send</button>
      </form>
    </div>
  );
}

Step 4: use() Hook — Read Promises & Context

The use() hook breaks React's most rigid rule: it can be called conditionally and inside loops. It was invented because useContext and async data reading had a fundamental limitation — you couldn't conditionally read context or await a promise inside a component without workarounds. use() reads Promises (triggering Suspense) and Context values anywhere in your component, enabling patterns like "only load admin context if user is admin" that were previously impossible without restructuring your component tree.

Unlike other hooks, use() can be called conditionally and inside loops:

import { use, Suspense } from 'react';

// Reading a Promise
function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
  const user = use(userPromise); // Suspends until resolved
  return <h1>{user.name}</h1>;
}

// Conditional usage (impossible with useContext!)
function Dashboard({ showAdmin }: { showAdmin: boolean }) {
  if (showAdmin) {
    const admin = use(AdminContext); // ✅ Conditional — only use() can do this
    return <AdminPanel data={admin} />;
  }
  return <UserDashboard />;
}

// Usage with Suspense boundary
function App() {
  const userPromise = fetchUser(userId); // Start fetching

  return (
    <Suspense fallback={<Spinner />}>
      <UserProfile userPromise={userPromise} />
    </Suspense>
  );
}

Step 5: useEffectEvent — Non-Reactive Values in Effects

useEffectEvent solves a pain point that every React developer has hit: you need the latest value of a prop inside an effect, but adding it to the dependency array causes the effect to re-run when it shouldn't. Before this hook, the workaround was a useRef that you manually updated — ugly, error-prone, and non-obvious. useEffectEvent creates an event handler that always reads the latest values but is never a reactive dependency, keeping your effects clean and correct.

Solves the "I need the latest value but don't want to re-run the effect" problem:

import { useEffect, useEffectEvent } from 'react';

function Chat({ roomId, theme }: { roomId: string; theme: string }) {
  // onConnected always reads latest `theme` without being a dependency
  const onConnected = useEffectEvent(() => {
    showNotification(`Connected to ${roomId}`, theme);
  });

  useEffect(() => {
    const connection = createConnection(roomId);
    connection.on('connected', onConnected);
    connection.connect();
    return () => connection.disconnect();
  }, [roomId]); // Only re-runs when roomId changes, NOT theme
}

Before useEffectEvent (workaround)

// Old pattern — messy useRef workaround
const themeRef = useRef(theme);
themeRef.current = theme;

useEffect(() => {
  connection.on('connected', () => {
    showNotification('Connected', themeRef.current);
  });
}, [roomId]);

Step 6: Activity Component — Hidden State Preservation

Before Activity, hiding a tab meant either unmounting it (losing all state, scroll position, and fetched data) or using display: none (which still triggers React re-renders and wastes CPU). The Activity component was built for tab-based UIs, step wizards, and keep-alive patterns where you want hidden content to preserve its state but not consume rendering resources. It tells React's scheduler to deprioritize updates to hidden subtrees, giving visible content priority.

Keep tabs alive without unmounting (replaces display: none hack):

import { Activity } from 'react';

function TabLayout({ activeTab }: { activeTab: string }) {
  return (
    <div>
      <Activity mode={activeTab === 'feed' ? 'visible' : 'hidden'}>
        <FeedTab /> {/* State, scroll, data all preserved when hidden */}
      </Activity>
      <Activity mode={activeTab === 'profile' ? 'visible' : 'hidden'}>
        <ProfileTab />
      </Activity>
      <Activity mode={activeTab === 'settings' ? 'visible' : 'hidden'}>
        <SettingsTab />
      </Activity>
    </div>
  );
}

Why not just display: none?

  • CSS hiding still triggers React re-renders on state updates
  • Activity tells React to defer updates to hidden subtrees — better performance

Step 7: ref as a Prop (No More forwardRef)

forwardRef was one of React's most confusing APIs — wrapping a component in forwardRef just to pass a ref through was verbose, killed type inference, and confused beginners. The React team realized that ref is conceptually just a prop, so in React 19, function components can accept ref as a regular prop. This simplifies component APIs, removes a layer of indirection, and makes TypeScript inference work naturally. forwardRef still works but triggers deprecation warnings.

// React 19 — ref is just a regular prop
function Input({ ref, ...props }: { ref?: React.Ref<HTMLInputElement> }) {
  return <input ref={ref} {...props} />;
}

// Usage — just pass ref directly
function Form() {
  const inputRef = useRef<HTMLInputElement>(null);
  return <Input ref={inputRef} placeholder="Enter name" />;
}

// forwardRef still works but shows deprecation warning

React 18 vs 19 Quick Comparison

Feature React 18 React 19
Memoization Manual (useMemo, useCallback, memo) Compiler handles automatically
Form mutations useState + loading + error state useActionState (3 lines)
Optimistic UI Third-party (SWR, TanStack) useOptimistic built-in
Ref forwarding forwardRef() required ref is a regular prop
Reading Promises Not possible in render use() hook
Hidden tabs CSS display: none (still re-renders) <Activity mode="hidden">
Non-reactive effect values useRef workaround useEffectEvent

Interview Questions

  1. What's the React Compiler and how does it work?

    • A Babel plugin that statically analyzes your code and inserts memoization at build time. It replaces manual useMemo, useCallback, and React.memo for components that follow the Rules of React.
  2. What's the difference between useActionState and useTransition?

    • useTransition wraps any async state update as non-blocking. useActionState is specifically for form actions — it manages the action function, its return state, and isPending in one hook.
  3. When would you use useOptimistic?

    • For immediate UI feedback during mutations (add to list, like button, send message). The UI updates instantly, then reconciles when the server responds. If the request fails, React reverts to the real state.
  4. How is use() different from other hooks?

    • It can be called conditionally and inside loops — violating the normal rules of hooks. It reads Promises (suspending until resolved) or Context values. It enables conditional data dependencies.
React Hooks Deep DiveReact Performance & Patterns

On this page

  • TL;DR
  • Step 1: React Compiler (Auto-Memoization)
  • Enabling the Compiler
  • What the Compiler WON'T optimize
  • Step 2: useActionState — Form Handling
  • Step 3: useOptimistic — Instant UI Updates
  • Step 4: use() Hook — Read Promises & Context
  • Step 5: useEffectEvent — Non-Reactive Values in Effects
  • Before useEffectEvent (workaround)
  • Step 6: Activity Component — Hidden State Preservation
  • Step 7: ref as a Prop (No More forwardRef)
  • React 18 vs 19 Quick Comparison
  • Interview Questions