React 19

12 min read
Rapid overview

React 19 - Complete Guide

Master all new features in React 19 for senior-level interviews.

Table of Contents


Overview

React 19 brings major improvements focusing on:

  1. Automatic memoization via the React Compiler
  2. Actions for handling async operations and form submissions
  3. Server Components first-class support
  4. Better DX with simplified APIs and improved error messages

React Compiler

The React Compiler (formerly React Forget) automatically memoizes components and hooks, eliminating the need for manual useMemo, useCallback, and React.memo.

Before (Manual Memoization)

// React 18 - manual optimization
function ProductList({ products, onAddToCart }) {
  const sortedProducts = useMemo(
    () => products.sort((a, b) => a.price - b.price),
    [products]
  );

  const handleAdd = useCallback(
    (id) => onAddToCart(id),
    [onAddToCart]
  );

  return (
    <ul>
      {sortedProducts.map(product => (
        <ProductItem
          key={product.id}
          product={product}
          onAdd={handleAdd}
        />
      ))}
    </ul>
  );
}

const ProductItem = React.memo(({ product, onAdd }) => (
  <li>
    {product.name} - ${product.price}
    <button onClick={() => onAdd(product.id)}>Add</button>
  </li>
));

After (Compiler Handles It)

// React 19 - compiler optimizes automatically
function ProductList({ products, onAddToCart }) {
  const sortedProducts = products.sort((a, b) => a.price - b.price);

  return (
    <ul>
      {products.map(product => (
        <ProductItem
          key={product.id}
          product={product}
          onAdd={(id) => onAddToCart(id)}
        />
      ))}
    </ul>
  );
}

function ProductItem({ product, onAdd }) {
  return (
    <li>
      {product.name} - ${product.price}
      <button onClick={() => onAdd(product.id)}>Add</button>
    </li>
  );
}

Enabling the Compiler

// babel.config.js
module.exports = {
  plugins: [
    ['babel-plugin-react-compiler', {
      // Compiler options
    }]
  ]
};

// Or with Vite
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import reactCompiler from 'babel-plugin-react-compiler';

export default defineConfig({
  plugins: [
    react({
      babel: {
        plugins: [reactCompiler]
      }
    })
  ]
});

Actions

Actions are async functions that handle data mutations, form submissions, and async state transitions. They integrate with Suspense and provide built-in pending states.

Basic Action Pattern

// React 19 action pattern
function UpdateProfile() {
  const [error, setError] = useState(null);
  const [isPending, startTransition] = useTransition();

  const handleSubmit = async (formData) => {
    startTransition(async () => {
      const result = await updateProfile(formData);
      if (result.error) {
        setError(result.error);
      }
    });
  };

  return (
    <form action={handleSubmit}>
      <input name="name" />
      <button disabled={isPending}>
        {isPending ? 'Saving...' : 'Save'}
      </button>
      {error && <p className="error">{error}</p>}
    </form>
  );
}

Form Actions

// Direct form action
async function createPost(formData) {
  'use server'; // Server Action marker

  const title = formData.get('title');
  const content = formData.get('content');

  await db.posts.create({ title, content });
  revalidatePath('/posts');
}

function NewPostForm() {
  return (
    <form action={createPost}>
      <input name="title" placeholder="Title" />
      <textarea name="content" placeholder="Content" />
      <button type="submit">Create Post</button>
    </form>
  );
}

useActionState

useActionState (formerly useFormState) manages form state and async actions with automatic pending states.

import { useActionState } from 'react';

async function submitForm(previousState, formData) {
  const email = formData.get('email');

  if (!email.includes('@')) {
    return { error: 'Invalid email address' };
  }

  await api.subscribe(email);
  return { success: true, message: 'Subscribed!' };
}

function NewsletterForm() {
  const [state, formAction, isPending] = useActionState(submitForm, null);

  return (
    <form action={formAction}>
      <input
        type="email"
        name="email"
        placeholder="your@email.com"
        disabled={isPending}
      />
      <button type="submit" disabled={isPending}>
        {isPending ? 'Subscribing...' : 'Subscribe'}
      </button>

      {state?.error && <p className="error">{state.error}</p>}
      {state?.success && <p className="success">{state.message}</p>}
    </form>
  );
}

With Server Actions

// Server Action
async function addToCart(prevState, formData) {
  'use server';

  const productId = formData.get('productId');

  try {
    await db.cart.add(productId);
    revalidatePath('/cart');
    return { success: true };
  } catch (error) {
    return { error: error.message };
  }
}

// Client Component
function AddToCartButton({ productId }) {
  const [state, formAction, isPending] = useActionState(addToCart, null);

  return (
    <form action={formAction}>
      <input type="hidden" name="productId" value={productId} />
      <button disabled={isPending}>
        {isPending ? 'Adding...' : 'Add to Cart'}
      </button>
      {state?.error && <span className="error">{state.error}</span>}
    </form>
  );
}

useFormStatus

useFormStatus provides pending state for form submissions from within form components.

import { useFormStatus } from 'react-dom';

function SubmitButton() {
  const { pending, data, method, action } = useFormStatus();

  return (
    <button type="submit" disabled={pending}>
      {pending ? 'Submitting...' : 'Submit'}
    </button>
  );
}

function ContactForm() {
  async function handleSubmit(formData) {
    await api.contact(formData);
  }

  return (
    <form action={handleSubmit}>
      <input name="name" placeholder="Name" />
      <input name="email" placeholder="Email" />
      <textarea name="message" placeholder="Message" />
      <SubmitButton /> {/* Automatically knows form pending state */}
    </form>
  );
}

Multiple Form Fields

function FormFields() {
  const { pending } = useFormStatus();

  return (
    <>
      <input name="username" disabled={pending} />
      <input name="password" type="password" disabled={pending} />
      <SubmitButton />
    </>
  );
}

function LoginForm({ action }) {
  return (
    <form action={action}>
      <FormFields />
    </form>
  );
}

useOptimistic

useOptimistic enables optimistic UI updates that revert automatically if the async action fails.

import { useOptimistic } from 'react';

function TodoList({ todos, addTodo }) {
  const [optimisticTodos, addOptimisticTodo] = useOptimistic(
    todos,
    (currentTodos, newTodo) => [...currentTodos, newTodo]
  );

  async function handleSubmit(formData) {
    const title = formData.get('title');
    const newTodo = { id: Date.now(), title, pending: true };

    // Immediately update UI
    addOptimisticTodo(newTodo);

    // Actual API call
    await addTodo(title);
  }

  return (
    <div>
      <ul>
        {optimisticTodos.map(todo => (
          <li
            key={todo.id}
            style={{ opacity: todo.pending ? 0.5 : 1 }}
          >
            {todo.title}
          </li>
        ))}
      </ul>

      <form action={handleSubmit}>
        <input name="title" placeholder="New todo" />
        <button type="submit">Add</button>
      </form>
    </div>
  );
}

Like Button Example

function LikeButton({ postId, initialLiked, initialCount }) {
  const [liked, setLiked] = useState(initialLiked);
  const [count, setCount] = useState(initialCount);

  const [optimisticLiked, toggleOptimisticLike] = useOptimistic(
    liked,
    (current) => !current
  );

  const [optimisticCount, updateOptimisticCount] = useOptimistic(
    count,
    (current, change) => current + change
  );

  async function handleClick() {
    const newLiked = !liked;
    const change = newLiked ? 1 : -1;

    toggleOptimisticLike();
    updateOptimisticCount(change);

    try {
      await api.toggleLike(postId);
      setLiked(newLiked);
      setCount(count + change);
    } catch {
      // Optimistic state automatically reverts on error
    }
  }

  return (
    <button onClick={handleClick}>
      {optimisticLiked ? '❤️' : '🤍'} {optimisticCount}
    </button>
  );
}

use Hook

The use hook reads resources (Promises, Context) during render. It can be called conditionally, unlike other hooks.

Reading Promises

import { use, Suspense } from 'react';

function UserProfile({ userPromise }) {
  // Suspends until promise resolves
  const user = use(userPromise);

  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  );
}

function App() {
  const userPromise = fetchUser(userId);

  return (
    <Suspense fallback={<div>Loading...</div>}>
      <UserProfile userPromise={userPromise} />
    </Suspense>
  );
}

Conditional Use

function Comments({ commentsPromise, showComments }) {
  // Can be called conditionally!
  if (!showComments) {
    return null;
  }

  const comments = use(commentsPromise);

  return (
    <ul>
      {comments.map(comment => (
        <li key={comment.id}>{comment.text}</li>
      ))}
    </ul>
  );
}

Reading Context Conditionally

function ThemeIcon({ forceLight }) {
  // Can conditionally read context
  if (forceLight) {
    return <SunIcon />;
  }

  const theme = use(ThemeContext);
  return theme === 'dark' ? <MoonIcon /> : <SunIcon />;
}

Server Components

React Server Components (RSC) run on the server and stream to the client. They can directly access databases, file systems, and other server resources.

Server Component

// app/posts/page.jsx (Server Component by default)
import { db } from '@/lib/db';

async function PostsPage() {
  // Direct database access - no API needed
  const posts = await db.posts.findMany({
    orderBy: { createdAt: 'desc' }
  });

  return (
    <div>
      <h1>Posts</h1>
      {posts.map(post => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.excerpt}</p>
          <LikeButton postId={post.id} /> {/* Client Component */}
        </article>
      ))}
    </div>
  );
}

export default PostsPage;

Client Component

// components/LikeButton.jsx
'use client'; // Mark as Client Component

import { useState } from 'react';

export function LikeButton({ postId }) {
  const [liked, setLiked] = useState(false);

  return (
    <button onClick={() => setLiked(!liked)}>
      {liked ? '❤️' : '🤍'}
    </button>
  );
}

Patterns

// Server Component with async data
async function ProductPage({ params }) {
  const product = await getProduct(params.id);
  const reviews = await getReviews(params.id);

  return (
    <div>
      <ProductDetails product={product} />
      <Suspense fallback={<ReviewsSkeleton />}>
        <Reviews reviews={reviews} />
      </Suspense>
      <AddToCartButton productId={params.id} /> {/* Client */}
    </div>
  );
}

// Passing Server data to Client Components
async function Dashboard() {
  const user = await getCurrentUser();
  const stats = await getUserStats(user.id);

  return (
    <DashboardClient
      initialUser={user}
      initialStats={stats}
    />
  );
}

Server Actions

Server Actions are async functions that run on the server, callable from Client Components.

// actions/user.js
'use server';

import { db } from '@/lib/db';
import { revalidatePath } from 'next/cache';

export async function updateProfile(formData) {
  const name = formData.get('name');
  const bio = formData.get('bio');

  await db.user.update({
    where: { id: getCurrentUserId() },
    data: { name, bio }
  });

  revalidatePath('/profile');
}

export async function deleteAccount() {
  await db.user.delete({
    where: { id: getCurrentUserId() }
  });

  redirect('/');
}
// components/ProfileForm.jsx
'use client';

import { updateProfile } from '@/actions/user';
import { useActionState } from 'react';

export function ProfileForm({ user }) {
  const [state, formAction, isPending] = useActionState(
    updateProfile,
    null
  );

  return (
    <form action={formAction}>
      <input name="name" defaultValue={user.name} />
      <textarea name="bio" defaultValue={user.bio} />
      <button disabled={isPending}>
        {isPending ? 'Saving...' : 'Save'}
      </button>
    </form>
  );
}

Document Metadata

React 19 supports rendering <title>, <meta>, and <link> tags directly in components.

function BlogPost({ post }) {
  return (
    <article>
      <title>{post.title} | My Blog</title>
      <meta name="description" content={post.excerpt} />
      <meta property="og:title" content={post.title} />
      <meta property="og:image" content={post.image} />
      <link rel="canonical" href={`https://myblog.com/posts/${post.slug}`} />

      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </article>
  );
}

function ProductPage({ product }) {
  return (
    <>
      <title>{product.name} - Shop</title>
      <meta name="description" content={product.description} />
      <meta name="robots" content="index, follow" />

      <ProductDetails product={product} />
    </>
  );
}

Stylesheets Support

React 19 adds built-in support for stylesheets with automatic ordering and deduplication.

function Component() {
  return (
    <>
      {/* precedence determines order */}
      <link rel="stylesheet" href="/styles/base.css" precedence="default" />
      <link rel="stylesheet" href="/styles/theme.css" precedence="high" />

      <div className="styled-component">Content</div>
    </>
  );
}

// Suspense waits for stylesheets
function StyledSection() {
  return (
    <Suspense fallback={<Skeleton />}>
      <link rel="stylesheet" href="/styles/section.css" precedence="default" />
      <section>Styled content</section>
    </Suspense>
  );
}

Async Scripts

React 19 supports async script loading with deduplication.

function AnalyticsComponent() {
  return (
    <>
      <script async src="https://analytics.example.com/script.js" />
      <div>Analytics enabled</div>
    </>
  );
}

function PaymentForm() {
  return (
    <>
      <script async src="https://js.stripe.com/v3/" />
      <form>
        <div id="card-element" />
        <button>Pay</button>
      </form>
    </>
  );
}

Resource Preloading

New APIs for preloading resources to improve performance.

import { prefetchDNS, preconnect, preload, preinit } from 'react-dom';

function App() {
  // DNS prefetch for external domains
  prefetchDNS('https://api.example.com');

  // Preconnect to CDN
  preconnect('https://cdn.example.com');

  // Preload critical resources
  preload('/fonts/inter.woff2', { as: 'font', type: 'font/woff2' });
  preload('/hero-image.jpg', { as: 'image' });

  // Preinit scripts (load and execute)
  preinit('https://analytics.example.com/script.js', { as: 'script' });

  return <MainContent />;
}

function ImageGallery({ images }) {
  // Preload next image
  const nextImage = images[currentIndex + 1];
  if (nextImage) {
    preload(nextImage.src, { as: 'image' });
  }

  return <img src={images[currentIndex].src} />;
}

ref as a Prop

Function components can now receive ref as a regular prop, no forwardRef needed.

// React 18 - forwardRef required
const Input = forwardRef((props, ref) => (
  <input ref={ref} {...props} />
));

// React 19 - ref is just a prop
function Input({ ref, ...props }) {
  return <input ref={ref} {...props} />;
}

// Usage unchanged
function Form() {
  const inputRef = useRef(null);

  return (
    <>
      <Input ref={inputRef} placeholder="Enter text" />
      <button onClick={() => inputRef.current.focus()}>
        Focus
      </button>
    </>
  );
}

With TypeScript

// React 19 - simpler typing
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
  ref?: React.Ref<HTMLInputElement>;
}

function Input({ ref, ...props }: InputProps) {
  return <input ref={ref} {...props} />;
}

Improved Error Reporting

React 19 provides better error messages and avoids duplicate logging.

Hydration Errors

// React 19 shows clear diff for hydration mismatches
// Error: Hydration mismatch
// Server: <div>Hello World</div>
// Client: <div>Hello Client</div>

Error Boundaries

function ErrorBoundary({ children, fallback }) {
  return (
    <ErrorBoundaryComponent
      fallback={fallback}
      onError={(error, errorInfo) => {
        // React 19 provides better error info
        console.error('Error:', error);
        console.error('Component Stack:', errorInfo.componentStack);
      }}
    >
      {children}
    </ErrorBoundaryComponent>
  );
}

Context as Provider

Context can now be rendered directly as a provider.

// React 18
const ThemeContext = createContext('light');

function App() {
  return (
    <ThemeContext.Provider value="dark">
      <Content />
    </ThemeContext.Provider>
  );
}

// React 19 - simpler syntax
function App() {
  return (
    <ThemeContext value="dark">
      <Content />
    </ThemeContext>
  );
}

Cleanup Functions for Refs

Ref callbacks can now return cleanup functions.

// React 18 - manual cleanup
function Component() {
  const ref = useCallback((node) => {
    if (node) {
      const observer = new ResizeObserver(handleResize);
      observer.observe(node);
      // No way to cleanup!
    }
  }, []);

  return <div ref={ref} />;
}

// React 19 - cleanup function
function Component() {
  const ref = useCallback((node) => {
    if (!node) return;

    const observer = new ResizeObserver(handleResize);
    observer.observe(node);

    // Cleanup when element is removed
    return () => {
      observer.disconnect();
    };
  }, []);

  return <div ref={ref} />;
}

useDeferredValue Initial Value

useDeferredValue now accepts an initial value for first render.

// React 18
function SearchResults({ query }) {
  const deferredQuery = useDeferredValue(query);
  // First render uses query immediately

  return <Results query={deferredQuery} />;
}

// React 19 - initial value
function SearchResults({ query }) {
  // First render uses '' (shows cached/placeholder)
  // Then updates to query
  const deferredQuery = useDeferredValue(query, '');

  return <Results query={deferredQuery} />;
}

Questions & Answers

Q: What is the React Compiler and do I need to change my code?

A: The React Compiler automatically adds memoization. You don't need to change code, but you can remove manual useMemo, useCallback, and React.memo calls as the compiler handles optimization.

Q: What's the difference between useActionState and useFormStatus?

A: useActionState manages the full action lifecycle (state, action function, pending). useFormStatus only provides pending state and must be used inside a form's child component.

Q: When should I use useOptimistic?

A: Use it when you want immediate UI feedback before an async operation completes. It automatically reverts if the action fails.

Q: Can I use Server Components without a framework?

A: Server Components require a bundler setup. Frameworks like Next.js, Remix, or Waku provide the necessary infrastructure. Manual setup is complex.

Q: What's the difference between Server Components and Server Actions?

A: Server Components are components that render on the server (read data, no interactivity). Server Actions are functions that run on the server and can be called from Client Components (mutations, form handling).

Q: Do I still need forwardRef in React 19?

A: No, function components can receive ref as a regular prop. forwardRef still works for backwards compatibility but is no longer needed.

Q: How does the use hook differ from await?

A: use integrates with Suspense - it suspends the component while the promise resolves. It can also be called conditionally, unlike other hooks. It's for rendering, while await is for effects/actions.

Q: What happened to useFormState?

A: It was renamed to useActionState in the final React 19 release. The functionality is the same.

Q: Should I migrate all my forms to use Actions?

A: Actions work best for forms that need server interaction or complex state. Simple client-only forms can continue using controlled components with useState.

Q: How do I handle errors with Server Actions?

A: Return error state from the action and handle it in the component. Use try/catch in the action and return { error: message } that the component can display.