Skip to main content
← All Articles Β· Β· 13 min read

React 19: Complete Guide to New Features in 2026

Everything new in React 19: use() hook, Actions, optimistic updates, ref as prop, form actions, and the React Compiler. Production-ready patterns with TypeScript examples.

reactreact-19typescriptfrontendweb-development

React 19 shipped as stable in late 2024 and by 2026 it’s the foundation of every new React project. The release focused on one theme: making async patterns feel native to React. Server-first data fetching, optimistic mutations, and form handling are now first-class citizens.

This guide covers every meaningful change with production-ready examples β€” not just descriptions.


The use() Hook

use() is the most versatile addition in React 19. It reads a resource during render β€” a Promise or a Context β€” suspending the component until the resource resolves.

Reading a Promise

import { use, Suspense } from 'react';

// Fetch the data OUTSIDE the component
const userPromise = fetch('/api/user').then(r => r.json());

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

  return <h1>Hello, {user.name}</h1>;
}

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

Unlike useEffect + useState, use() integrates with Suspense β€” the component doesn’t render until data is ready. The parent’s fallback shows during the wait.

Critical: create the Promise outside the component. Creating it inside would create a new Promise on every render.

Reading Context Conditionally

The old useContext couldn’t be called conditionally. use() can:

import { use } from 'react';

function Message({ show }: { show: boolean }) {
  if (!show) return null; // ← early return

  // This was impossible with useContext
  const theme = use(ThemeContext);
  return <p style={{ color: theme.primary }}>Hello</p>;
}

Actions and useTransition

React 19 formalizes the concept of β€œActions” β€” async functions passed to transitions. React tracks their pending state automatically.

import { useTransition } from 'react';

function UpdateNameForm({ userId }: { userId: string }) {
  const [isPending, startTransition] = useTransition();
  const [error, setError] = useState<string | null>(null);

  function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
    event.preventDefault();
    const name = new FormData(event.currentTarget).get('name') as string;

    startTransition(async () => {
      const result = await updateUserName(userId, name);
      if (result.error) {
        setError(result.error);
      }
    });
  }

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

isPending is automatically true while the async function inside startTransition is running. No useState needed for loading state.


useActionState

useActionState is purpose-built for managing form state through a Server Action or async function. It’s the successor to React DOM’s useFormState.

import { useActionState } from 'react';

interface State {
  message: string | null;
  errors: Record<string, string[]> | null;
}

async function submitContactForm(prevState: State, formData: FormData): Promise<State> {
  const email = formData.get('email') as string;
  const message = formData.get('message') as string;

  if (!email.includes('@')) {
    return { message: null, errors: { email: ['Invalid email address'] } };
  }

  await sendEmail({ email, message });
  return { message: 'Message sent!', errors: null };
}

export function ContactForm() {
  const [state, formAction, isPending] = useActionState(
    submitContactForm,
    { message: null, errors: null }
  );

  return (
    <form action={formAction}>
      <input name="email" type="email" required />
      {state.errors?.email?.map(e => <p key={e} className="text-red-500">{e}</p>)}

      <textarea name="message" required />

      <button type="submit" disabled={isPending}>
        {isPending ? 'Sending...' : 'Send Message'}
      </button>

      {state.message && <p className="text-green-500">{state.message}</p>}
    </form>
  );
}

Three values returned: [state, formAction, isPending].


useOptimistic

Optimistic UI updates have always required complex state management. React 19 solves it with useOptimistic.

import { useOptimistic, useTransition } from 'react';

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

export function MessageThread({ messages }: { messages: Message[] }) {
  const [optimisticMessages, addOptimisticMessage] = useOptimistic(
    messages,
    (current, newMessage: Message) => [...current, newMessage]
  );
  const [isPending, startTransition] = useTransition();

  async function handleSend(text: string) {
    const tempId = crypto.randomUUID();

    startTransition(async () => {
      // Update UI immediately with "sending" indicator
      addOptimisticMessage({ id: tempId, text, sending: true });

      // Send to server
      await sendMessage(text);
      // Server revalidation replaces the optimistic message
    });
  }

  return (
    <div>
      {optimisticMessages.map(msg => (
        <div key={msg.id} className={msg.sending ? 'opacity-60' : ''}>
          {msg.text} {msg.sending && <span>Sending...</span>}
        </div>
      ))}
      <MessageInput onSend={handleSend} disabled={isPending} />
    </div>
  );
}

If the server call fails, optimisticMessages automatically reverts to the original messages prop. No manual rollback code.


ref as a Prop

Before React 19, forwarding refs required forwardRef() boilerplate. Now ref works like any other prop.

// Before React 19
const Input = forwardRef<HTMLInputElement, InputProps>((props, ref) => (
  <input ref={ref} {...props} />
));

// React 19 β€” no forwardRef needed
function Input({ ref, ...props }: InputProps & { ref?: React.Ref<HTMLInputElement> }) {
  return <input ref={ref} {...props} />;
}

// Usage
const inputRef = useRef<HTMLInputElement>(null);
<Input ref={inputRef} type="text" />

The old forwardRef API still works for backward compatibility, but new components should use the prop-based approach.


Improved Hydration Error Messages

React 19 dramatically improved hydration mismatch error messages. Instead of:

Error: Hydration failed because the initial UI does not match what was rendered on the server.

You now get a diff showing exactly which element mismatches and the server vs. client values. This alone saves hours of debugging in complex SSR applications.


Document Metadata

React 19 adds native support for rendering <title>, <meta>, and <link> tags directly in components β€” they automatically hoist to <head>.

function BlogPost({ post }: { post: Post }) {
  return (
    <article>
      <title>{post.title} | DevPlaybook</title>
      <meta name="description" content={post.excerpt} />
      <link rel="canonical" href={`https://devplaybook.cc/blog/${post.slug}`} />

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

No need for next/head or react-helmet in new projects. Works with SSR β€” React renders the metadata server-side, so crawlers see it immediately.


Stylesheet Priority Management

React 19 manages stylesheet loading order via a precedence prop on <link>:

function Component() {
  return (
    <>
      <link rel="stylesheet" href="/base.css" precedence="default" />
      <link rel="stylesheet" href="/theme.css" precedence="high" />
      <div className="themed-content">...</div>
    </>
  );
}

React deduplicates stylesheets across the page and ensures loading order matches precedence levels.


Async Script Support

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

React 19 deduplicates async scripts β€” if multiple components render the same src, the script loads once regardless. No more useEffect + document.createElement boilerplate.


The React Compiler

Shipped alongside React 19, the React Compiler automatically applies optimizations that you previously needed useMemo, useCallback, and React.memo for.

// Before: manual memoization required
const expensiveValue = useMemo(() => computeExpensive(a, b), [a, b]);
const stableCallback = useCallback(() => handleClick(id), [id]);
const MemoComponent = React.memo(MyComponent);

// After: the compiler handles this automatically
// Write plain React β€” the compiler adds memoization where needed
const expensiveValue = computeExpensive(a, b);
const stableCallback = () => handleClick(id);
function MyComponent() { ... }

The compiler analyzes your code statically and inserts memoization at the right granularity β€” finer-grained than manual React.memo wrapping. Enable it in your Next.js config:

// next.config.js
module.exports = {
  experimental: {
    reactCompiler: true,
  },
};

The opt-out escape hatch:

function ComponentWithSideEffects() {
  'use no memo'; // ← opt this component out of compiler optimization
  // ...
}

Migration from React 18

Most codebases migrate without code changes. Breaking changes are minimal:

ChangeImpactAction
ReactDOM.render removedRareUse createRoot
useFormState β†’ useActionStateCommonRename import
forwardRef deprecatedOptionalMigrate gradually
React.FC implicit children removedCommonAdd { children: ReactNode } explicitly
Strict mode double-effect cleanupTestingUpdate test assertions

React 19 vs React 18: Performance

React 19 doesn’t change the core scheduling model, but the Compiler typically reduces re-renders by 30–50% in component trees with expensive calculations. The biggest wins come from:

  1. Automatic memoization of derived values
  2. Stable function references without useCallback
  3. use() eliminating waterfall data fetching patterns

The shift in React 19 is philosophical: async is first-class, not bolted on. Actions, use(), and useOptimistic let you write async code the way you think about it β€” without manually managing every loading/error/success state.

For hands-on practice, try the React hooks reference and TypeScript code formatter.

Free Download

50 CLI Commands That Save Hours Every Week

The exact Git shortcuts, file search tricks, Docker commands, and productivity hacks that senior developers use daily. Compiled from 5+ years of shipping production code.

  • Git mastery (10 cmds)
  • File & search wizardry
  • Docker & DevOps tricks
  • Process & network tools
  • Productivity boosters
  • macOS & Linux install guide

No spam. 1 email/week max. Unsubscribe anytime.

Found this guide useful? Check out our free developer tools.