TechLead

React 19 Features

use() hook, Actions, useFormStatus, useOptimistic, and the React Compiler

What's New in React 19

React 19 is a major release that introduces powerful new primitives for handling async operations, forms, and optimistic updates. These features simplify patterns that previously required complex boilerplate or external libraries.

πŸš€ Key React 19 Features

  • use() β€” Read promises and context in render
  • Actions β€” Async transitions with automatic pending states
  • useFormStatus() β€” Track form submission state
  • useOptimistic() β€” Optimistic UI updates
  • useActionState() β€” Manage action state and errors
  • React Compiler β€” Automatic memoization (no more manual useMemo/useCallback)
  • ref as prop β€” No more forwardRef needed

The use() Hook

use() lets you read the value of a Promise or Context directly during render. Unlike other hooks, use() can be called inside conditionals and loops.

import { use, Suspense } from 'react';

// Reading a Promise with use()
function UserProfile({ userPromise }) {
  // Suspends until the promise resolves
  const user = use(userPromise);

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

// Parent passes the promise (created outside render)
function App() {
  const userPromise = fetchUser(1); // returns a Promise

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

// use() with Context β€” replaces useContext()
function ThemedButton() {
  const theme = use(ThemeContext); // can be inside if/loops!

  return <button style={{ background: theme.primary }}>Click me</button>;
}

Actions and useTransition

Actions are async functions passed to startTransition. React 19 automatically handles pending states, errors, and optimistic updates for you.

import { useState, useTransition } from 'react';

function UpdateProfile() {
  const [name, setName] = useState('');
  const [isPending, startTransition] = useTransition();
  const [error, setError] = useState(null);

  async function handleSubmit() {
    setError(null);

    startTransition(async () => {
      // This async function is an "Action"
      const result = await updateProfile({ name });

      if (result.error) {
        setError(result.error);
        return;
      }
      // React automatically manages isPending
      // No need for manual loading state!
    });
  }

  return (
    <form onSubmit={(e) => { e.preventDefault(); handleSubmit(); }}>
      <input value={name} onChange={e => setName(e.target.value)} />
      <button disabled={isPending}>
        {isPending ? 'Saving...' : 'Save'}
      </button>
      {error && <p className="text-red-500">{error}</p>}
    </form>
  );
}

useActionState

useActionState manages the full lifecycle of a form action β€” state, pending status, and error handling in one hook:

import { useActionState } from 'react';

async function addTodo(previousState, formData) {
  const title = formData.get('title');

  if (!title) {
    return { error: 'Title is required', todos: previousState.todos };
  }

  const newTodo = await saveTodo({ title });
  return {
    error: null,
    todos: [...previousState.todos, newTodo],
  };
}

function TodoForm() {
  const [state, formAction, isPending] = useActionState(addTodo, {
    error: null,
    todos: [],
  });

  return (
    <div>
      <form action={formAction}>
        <input name="title" placeholder="New todo..." />
        <button disabled={isPending}>
          {isPending ? 'Adding...' : 'Add Todo'}
        </button>
        {state.error && <p className="text-red-500">{state.error}</p>}
      </form>

      <ul>
        {state.todos.map(todo => (
          <li key={todo.id}>{todo.title}</li>
        ))}
      </ul>
    </div>
  );
}

useFormStatus

useFormStatus gives child components access to the parent form's submission state. Must be called from a component inside a form:

import { useFormStatus } from 'react-dom';

// This component must be a child of a <form>
function SubmitButton() {
  const { pending, data, method, action } = useFormStatus();

  return (
    <button disabled={pending} className="btn">
      {pending ? (
        <span className="flex items-center gap-2">
          <Spinner /> Submitting...
        </span>
      ) : (
        'Submit'
      )}
    </button>
  );
}

// Usage
function ContactForm() {
  async function handleSubmit(formData) {
    await sendMessage(formData);
  }

  return (
    <form action={handleSubmit}>
      <input name="email" type="email" required />
      <textarea name="message" required />

      {/* SubmitButton reads the form's pending state */}
      <SubmitButton />
    </form>
  );
}

useOptimistic

useOptimistic lets you show an optimistic (predicted) state while an async action is in progress, then automatically reverts if it fails:

import { useOptimistic, useTransition } from 'react';

function MessageList({ messages, sendMessage }) {
  const [optimisticMessages, addOptimistic] = useOptimistic(
    messages,
    // Merge function: how to add the optimistic value
    (currentMessages, newMessage) => [
      ...currentMessages,
      { ...newMessage, sending: true },
    ]
  );

  const [, startTransition] = useTransition();

  async function handleSend(formData) {
    const text = formData.get('message');
    const optimisticMsg = { id: Date.now(), text, sending: true };

    startTransition(async () => {
      addOptimistic(optimisticMsg);
      await sendMessage(text); // If this fails, optimistic update reverts
    });
  }

  return (
    <div>
      {optimisticMessages.map(msg => (
        <div key={msg.id} className={msg.sending ? 'opacity-60' : ''}>
          {msg.text}
          {msg.sending && <span className="text-xs"> (sending...)</span>}
        </div>
      ))}

      <form action={handleSend}>
        <input name="message" />
        <button>Send</button>
      </form>
    </div>
  );
}

ref as a Prop (No More forwardRef)

In React 19, function components can accept ref as a regular prop β€” no more wrapping in forwardRef:

// React 18 β€” needed forwardRef
const Input = forwardRef(function Input(props, ref) {
  return <input ref={ref} {...props} />;
});

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

// Usage is the same
function Form() {
  const inputRef = useRef(null);
  return <Input ref={inputRef} placeholder="Type here" />;
}

The React Compiler (React Forget)

The React Compiler automatically optimizes your components by adding memoization where needed. It eliminates the need to manually use useMemo, useCallback, and React.memo.

// Before React Compiler β€” manual optimization
function ProductList({ products, onSelect }) {
  const sorted = useMemo(
    () => [...products].sort((a, b) => a.price - b.price),
    [products]
  );

  const handleClick = useCallback(
    (id) => onSelect(id),
    [onSelect]
  );

  return sorted.map(p => (
    <Product key={p.id} product={p} onClick={handleClick} />
  ));
}

// With React Compiler β€” just write natural code!
function ProductList({ products, onSelect }) {
  const sorted = [...products].sort((a, b) => a.price - b.price);

  return sorted.map(p => (
    <Product key={p.id} product={p} onClick={() => onSelect(p.id)} />
  ));
}
// The compiler figures out what needs memoization automatically!

βœ… Migration Tips

  • β€’ React 19 is backward compatible β€” existing code continues to work
  • β€’ Start by replacing forwardRef with ref-as-prop
  • β€’ Adopt useActionState for form handling in new code
  • β€’ Use useOptimistic to replace manual optimistic update patterns
  • β€’ The React Compiler is opt-in β€” enable it per file or folder
  • β€’ use() works great with Suspense for data fetching