React 19 Official Release - Complete Guide to Actions, use(), and New Hooks

2025.12.02

React 19.0 TypeScript 5.5+
Official Documentation

React 19 has officially released with many new features including Server Actions, new hooks, and significant improvements to form handling. This article explains React 19’s major new features with practical code examples.

React 19 Main New Features

Overview

flowchart TB
    subgraph React19["React 19"]
        subgraph Hooks["New Hooks"]
            H1["use() - Read Promise/Context"]
            H2["useActionState() - Form action state"]
            H3["useFormStatus() - Form submission state"]
            H4["useOptimistic() - Optimistic updates"]
        end

        subgraph Actions["Actions"]
            A1["Server Actions - Server-side functions"]
            A2["Client Actions - Client async processing"]
            A3["Form Integration - form action"]
        end

        subgraph Other["Other Improvements"]
            O1["ref as prop - No forwardRef needed"]
            O2["Document Metadata - Direct title etc."]
            O3["Stylesheet management - Order via precedence"]
            O4["Resource Preloading - prefetch/preload API"]
        end
    end

use() Hook

Reading Promises

use() is a new hook for reading Promises or Context during rendering.

// use() - Reading Promises
import { use, Suspense } from 'react';

// Data fetch function
async function fetchUser(id: string): Promise<User> {
  const response = await fetch(`/api/users/${id}`);
  return response.json();
}

// Component
function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
  // Use with Suspense
  const user = use(userPromise);

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

// Parent component
function UserPage({ userId }: { userId: string }) {
  // Pass Promise as props
  const userPromise = fetchUser(userId);

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

// Can call use() conditionally (unlike other hooks)
function ConditionalData({ shouldFetch, dataPromise }: {
  shouldFetch: boolean;
  dataPromise: Promise<Data>;
}) {
  if (!shouldFetch) {
    return <div>No data needed</div>;
  }

  // Can use after conditional branch
  const data = use(dataPromise);
  return <div>{data.value}</div>;
}

Reading Context

// use() - Reading Context
import { use, createContext } from 'react';

const ThemeContext = createContext<'light' | 'dark'>('light');

function ThemedButton() {
  // Can use use() instead of useContext()
  const theme = use(ThemeContext);

  return (
    <button className={theme === 'dark' ? 'bg-gray-800' : 'bg-white'}>
      Click me
    </button>
  );
}

// Conditional Context reading
function ConditionalTheme({ useTheme }: { useTheme: boolean }) {
  if (!useTheme) {
    return <button>Default Button</button>;
  }

  // Can use after conditional branch
  const theme = use(ThemeContext);
  return <button className={`theme-${theme}`}>Themed Button</button>;
}

Actions

Server Actions

// app/actions.ts
'use server';

import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';

// Server Action
export async function createPost(formData: FormData) {
  const title = formData.get('title') as string;
  const content = formData.get('content') as string;

  // Validation
  if (!title || title.length < 3) {
    return { error: 'Title must be at least 3 characters' };
  }

  // Save to database
  const post = await db.post.create({
    data: { title, content },
  });

  // Revalidate cache
  revalidatePath('/posts');

  // Redirect
  redirect(`/posts/${post.id}`);
}

// Update action
export async function updatePost(id: string, formData: FormData) {
  const title = formData.get('title') as string;
  const content = formData.get('content') as string;

  await db.post.update({
    where: { id },
    data: { title, content },
  });

  revalidatePath(`/posts/${id}`);
  return { success: true };
}

// Delete action
export async function deletePost(id: string) {
  await db.post.delete({ where: { id } });
  revalidatePath('/posts');
  redirect('/posts');
}

Form Integration

// app/posts/new/page.tsx
'use client';

import { useActionState } from 'react';
import { createPost } from '../actions';

export default function NewPostPage() {
  // useActionState - Form action state management
  const [state, formAction, isPending] = useActionState(
    createPost,
    { error: null }
  );

  return (
    <form action={formAction}>
      <div>
        <label htmlFor="title">Title</label>
        <input
          id="title"
          name="title"
          required
          disabled={isPending}
        />
      </div>

      <div>
        <label htmlFor="content">Content</label>
        <textarea
          id="content"
          name="content"
          disabled={isPending}
        />
      </div>

      {state?.error && (
        <p className="text-red-500">{state.error}</p>
      )}

      <button type="submit" disabled={isPending}>
        {isPending ? 'Posting...' : 'Post'}
      </button>
    </form>
  );
}

useFormStatus

// Get form submission status
import { useFormStatus } from 'react-dom';

function SubmitButton() {
  // Get parent <form>'s submission status
  const { pending, data, method, action } = useFormStatus();

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

// Use in form
function ContactForm() {
  async function submitForm(formData: FormData) {
    'use server';
    // Submit processing
  }

  return (
    <form action={submitForm}>
      <input name="email" type="email" required />
      <textarea name="message" required />
      {/* SubmitButton automatically gets parent form's status */}
      <SubmitButton />
    </form>
  );
}

useOptimistic - Optimistic Updates

// Implementing optimistic updates
import { useOptimistic, startTransition } from 'react';

interface Message {
  id: string;
  text: string;
  sending?: boolean;
}

function ChatMessages({ messages }: { messages: Message[] }) {
  // Optimistic state management
  const [optimisticMessages, addOptimisticMessage] = useOptimistic(
    messages,
    (state, newMessage: Message) => [
      ...state,
      { ...newMessage, sending: true },
    ]
  );

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

    // Add optimistically
    startTransition(() => {
      addOptimisticMessage({
        id: tempId,
        text,
        sending: true,
      });
    });

    // Send to server
    await fetch('/api/messages', {
      method: 'POST',
      body: JSON.stringify({ text }),
    });
  }

  return (
    <div>
      <ul>
        {optimisticMessages.map((message) => (
          <li
            key={message.id}
            className={message.sending ? 'opacity-50' : ''}
          >
            {message.text}
            {message.sending && <span> (Sending...)</span>}
          </li>
        ))}
      </ul>

      <form action={sendMessage}>
        <input name="message" required />
        <button type="submit">Send</button>
      </form>
    </div>
  );
}

Like Button Example

// Optimistic like button
function LikeButton({ postId, initialLiked, initialCount }: {
  postId: string;
  initialLiked: boolean;
  initialCount: number;
}) {
  const [{ liked, count }, setOptimistic] = useOptimistic(
    { liked: initialLiked, count: initialCount },
    (state, newLiked: boolean) => ({
      liked: newLiked,
      count: state.count + (newLiked ? 1 : -1),
    })
  );

  async function toggleLike() {
    const newLiked = !liked;

    // Update optimistically
    startTransition(() => {
      setOptimistic(newLiked);
    });

    // Send to server
    await fetch(`/api/posts/${postId}/like`, {
      method: newLiked ? 'POST' : 'DELETE',
    });
  }

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

ref as prop

In React 19, ref can be received as props without forwardRef.

// Before React 18: forwardRef required
const InputOld = forwardRef<HTMLInputElement, InputProps>((props, ref) => {
  return <input ref={ref} {...props} />;
});

// React 19: ref can be passed as normal prop
function Input({ ref, ...props }: InputProps & { ref?: React.Ref<HTMLInputElement> }) {
  return <input ref={ref} {...props} />;
}

// Usage
function Form() {
  const inputRef = useRef<HTMLInputElement>(null);

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

Document Metadata

Metadata can now be written directly within components.

// Direct metadata writing
function BlogPost({ post }: { post: Post }) {
  return (
    <article>
      {/* Automatically hoisted to document head */}
      <title>{post.title} - My Blog</title>
      <meta name="description" content={post.excerpt} />
      <meta property="og:title" content={post.title} />
      <meta property="og:description" content={post.excerpt} />
      <link rel="canonical" href={`https://myblog.com/posts/${post.slug}`} />

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

// Usage across multiple pages
function ProductPage({ product }: { product: Product }) {
  return (
    <div>
      <title>{product.name} | My Store</title>
      <meta name="description" content={product.description} />

      {/* Structured data */}
      <script type="application/ld+json">
        {JSON.stringify({
          '@context': 'https://schema.org',
          '@type': 'Product',
          name: product.name,
          description: product.description,
          price: product.price,
        })}
      </script>

      <h1>{product.name}</h1>
      <p>{product.description}</p>
    </div>
  );
}

Stylesheet Management

// Stylesheet priority management
function ComponentWithStyles() {
  return (
    <>
      {/* Control load order with precedence */}
      <link
        rel="stylesheet"
        href="/styles/base.css"
        precedence="default"
      />
      <link
        rel="stylesheet"
        href="/styles/components.css"
        precedence="default"
      />
      <link
        rel="stylesheet"
        href="/styles/utilities.css"
        precedence="high"
      />

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

// Dynamic stylesheets
function ThemeSwitcher({ theme }: { theme: 'light' | 'dark' }) {
  return (
    <>
      <link
        rel="stylesheet"
        href={`/themes/${theme}.css`}
        precedence="high"
      />
      <div>Themed content</div>
    </>
  );
}

Resource Preloading

// Resource preloading API
import { prefetchDNS, preconnect, preload, preinit } from 'react-dom';

function ResourceHints() {
  // DNS prefetch
  prefetchDNS('https://api.example.com');

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

  // Resource preload
  preload('/fonts/custom.woff2', {
    as: 'font',
    type: 'font/woff2',
    crossOrigin: 'anonymous',
  });

  // Script preinit
  preinit('/scripts/analytics.js', {
    as: 'script',
  });

  return <div>Content</div>;
}

// Image preloading
function ImageGallery({ images }: { images: string[] }) {
  // Preload next images
  useEffect(() => {
    images.slice(1, 4).forEach((src) => {
      preload(src, { as: 'image' });
    });
  }, [images]);

  return (
    <div>
      {images.map((src) => (
        <img key={src} src={src} alt="" />
      ))}
    </div>
  );
}

React Compiler (Experimental)

// Automatic memoization with React Compiler

// Before: Manual useMemo/useCallback required
function ProductListOld({ products, onSelect }: Props) {
  const sortedProducts = useMemo(
    () => [...products].sort((a, b) => a.price - b.price),
    [products]
  );

  const handleSelect = useCallback(
    (id: string) => onSelect(id),
    [onSelect]
  );

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

// After: React Compiler auto-memoizes
function ProductList({ products, onSelect }: Props) {
  // No manual memoization needed - compiler optimizes
  const sortedProducts = [...products].sort((a, b) => a.price - b.price);

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

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

Improved Error Handling

// Improved error display

// Detailed hydration error display
// React 19 shows specific differences

// Improved ErrorBoundary
class ErrorBoundary extends React.Component<
  { children: React.ReactNode; fallback: React.ReactNode },
  { hasError: boolean; error: Error | null }
> {
  state = { hasError: false, error: null };

  static getDerivedStateFromError(error: Error) {
    return { hasError: true, error };
  }

  componentDidCatch(error: Error, info: React.ErrorInfo) {
    // React 19: More detailed stack trace
    console.error('Error:', error);
    console.error('Component Stack:', info.componentStack);
  }

  render() {
    if (this.state.hasError) {
      return this.props.fallback;
    }
    return this.props.children;
  }
}

// Usage
function App() {
  return (
    <ErrorBoundary fallback={<ErrorPage />}>
      <MainContent />
    </ErrorBoundary>
  );
}

Migration Guide

Migrating from React 18 to 19

# Update packages
npm install react@19 react-dom@19

# TypeScript type definitions
npm install -D @types/react@19 @types/react-dom@19
// Main changes

// 1. forwardRef → normal prop
// Before
const Input = forwardRef<HTMLInputElement, Props>((props, ref) => (
  <input ref={ref} {...props} />
));

// After
function Input({ ref, ...props }: Props & { ref?: Ref<HTMLInputElement> }) {
  return <input ref={ref} {...props} />;
}

// 2. useContext → use (optional)
// Before
const theme = useContext(ThemeContext);

// After (convenient for conditional use)
const theme = use(ThemeContext);

// 3. Deprecated API removal
// - defaultProps (function components)
// - propTypes
// - createFactory
// - render (react-dom)

// defaultProps alternative
// Before
function Button({ size = 'medium' }) { ... }
Button.defaultProps = { size: 'medium' };

// After (use default parameters)
function Button({ size = 'medium' }: { size?: 'small' | 'medium' | 'large' }) {
  // ...
}

Summary

React 19 provides many new features that significantly improve developer experience.

Key New Features

FeaturePurpose
use()Read Promise/Context
useActionStateForm action state management
useFormStatusGet submission status
useOptimisticOptimistic updates
Server ActionsServer-side processing
ref as propNo forwardRef needed

Migration Recommendations

  • New projects: Adopt React 19
  • Existing projects: Migrate gradually
  • Server Actions: Combine with Next.js 14+

React 19 enables more intuitive and faster React application development.

← Back to list