React 19 · How it works

2 min read
Senior4 min read
Rapid overview

How it works

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

Table of Contents


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} />;
}

See also