React

React 19: Every New Feature Developers Need to Know

React 19 is stable. Here's every new feature — Actions, useActionState, useOptimistic, Server Components, ref as prop, and more — with real code examples.

April 9, 20269 min read
Share:

React 19 dropped stable in December 2024, and if you haven't upgraded yet, you're leaving real developer experience improvements on the table. Not theoretical improvements — actual reductions in boilerplate for patterns you write every single week.

Forms, async state, optimistic updates, refs, document metadata. React 19 fixes the awkward parts of all of them. Here's what changed, why it matters, and how to use it.

Actions — The Biggest DX Change in React 19

This is the one that will immediately affect your day-to-day code.

Before React 19, handling async form submissions required wiring up multiple useState calls manually:

// Before React 19 — boilerplate everywhere
function ContactForm() {
  const [status, setStatus] = useState('idle')
  const [error, setError] = useState<string | null>(null)
 
  async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault()
    setStatus('loading')
    try {
      await submitContact(new FormData(e.currentTarget))
      setStatus('success')
    } catch (err) {
      setError((err as Error).message)
      setStatus('idle')
    }
  }
 
  return (
    <form onSubmit={handleSubmit}>
      <input name="email" type="email" required />
      <button disabled={status === 'loading'}>
        {status === 'loading' ? 'Sending...' : 'Send'}
      </button>
      {status === 'error' && <p>{error}</p>}
    </form>
  )
}

Every form in your app looked like this. Three state variables minimum, a try/catch you had to remember to write, e.preventDefault() every time.

React 19 introduces Actions — async functions you pass directly to the action prop of a form. The framework handles the pending state, transitions, and error boundaries automatically. Pair them with useActionState and the pattern collapses significantly:

// React 19 with useActionState
import { useActionState } from 'react'
 
type FormState = { status: 'idle' | 'success' | 'error'; message?: string }
 
function ContactForm() {
  const [state, submitAction, isPending] = useActionState(
    async (prevState: FormState, formData: FormData): Promise<FormState> => {
      try {
        await submitContact(formData)
        return { status: 'success' }
      } catch (err) {
        return { status: 'error', message: (err as Error).message }
      }
    },
    { status: 'idle' }
  )
 
  return (
    <form action={submitAction}>
      <input name="email" type="email" required />
      <button disabled={isPending}>
        {isPending ? 'Sending...' : 'Send'}
      </button>
      {state.status === 'error' && <p>{state.message}</p>}
      {state.status === 'success' && <p>Message sent!</p>}
    </form>
  )
}

useActionState takes your async function and an initial state, and returns [currentState, actionDispatcher, isPending]. The isPending boolean is handled for you — no manual loading state. The form's native action prop now accepts an async function, which React intercepts and wraps in a transition automatically.

useFormStatus — Reading Pending State Across the Tree

useFormStatus solves a related problem: you want a submit button component that knows whether its parent form is pending, without prop-drilling.

import { useFormStatus } from 'react-dom'
 
function SubmitButton() {
  const { pending } = useFormStatus()
 
  return (
    <button type="submit" disabled={pending}>
      {pending ? 'Submitting...' : 'Submit'}
    </button>
  )
}
 
function MyForm() {
  return (
    <form action={myAction}>
      <input name="name" />
      <SubmitButton /> {/* automatically knows when the form is pending */}
    </form>
  )
}

This is particularly useful when your submit button lives in a shared component library. It reads from context — specifically the nearest parent <form> with an action — so no props needed.

useOptimistic — Instant UI Updates

Optimistic UI has always been a good idea in theory but annoying to implement cleanly. You'd need to manually roll back state on error, manage a separate "pending" list, and keep everything in sync. useOptimistic makes it a first-class pattern.

import { useOptimistic, useState } from 'react'
 
interface LikeButtonProps {
  postId: string
  initialLikes: number
}
 
function LikeButton({ postId, initialLikes }: LikeButtonProps) {
  const [likes, setLikes] = useState(initialLikes)
  const [optimisticLikes, addOptimisticLike] = useOptimistic(
    likes,
    (current: number, increment: number) => current + increment
  )
 
  async function handleLike() {
    addOptimisticLike(1) // immediately shows +1
    try {
      const newCount = await likePost(postId) // real network call
      setLikes(newCount) // update with server truth
    } catch {
      // optimistic state automatically reverts when the action completes
      // since setLikes is never called with new data
    }
  }
 
  return (
    <button onClick={handleLike}>
      {optimisticLikes} {optimisticLikes === 1 ? 'like' : 'likes'}
    </button>
  )
}

The key behavior: useOptimistic returns the optimistic value during the async operation. Once the operation settles (either succeeds or the component re-renders with real state), the optimistic value is replaced by the actual likes state. If your server call fails, you just don't update likes, and React shows the original value again.

When to use it: Any interaction where the success case is overwhelmingly likely and the delay would feel sluggish. Like buttons, follows, bookmarks, toggles.

When not to: Payment flows, destructive operations, anything where showing incorrect state has meaningful consequences.

The use() Hook — Read Anything in Render

use() is unlike any other hook. It can be called conditionally, inside loops, anywhere in your component body. It accepts a Promise or a Context object and suspends the component until the value is available.

import { use, Suspense } from 'react'
 
interface User {
  id: number
  name: string
  email: string
}
 
function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
  const user = use(userPromise) // suspends until the promise resolves
  return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
    </div>
  )
}
 
function App() {
  // Create the promise outside the component — important!
  // If created inside, it would re-create on every render
  const userPromise = fetchUser(1)
 
  return (
    <Suspense fallback={<div>Loading user...</div>}>
      <UserProfile userPromise={userPromise} />
    </Suspense>
  )
}

This is fundamentally different from useEffect data fetching. With useEffect, you render with empty state, run the effect, then re-render with data — always two renders minimum, with a flash of empty UI even if your Suspense boundary hides it. With use(), the component genuinely suspends and the Suspense fallback shows until data is ready.

use() also works with Context, and since it can be called conditionally, you can skip reading a context when it's not needed:

function ThemeAwareButton({ showTheme, children }) {
  if (showTheme) {
    const theme = use(ThemeContext) // conditional — valid with use()
    return <button style={{ background: theme.primary }}>{children}</button>
  }
  return <button>{children}</button>
}

This was impossible with useContext due to the Rules of Hooks.

ref as a Prop — Goodbye forwardRef

forwardRef was one of those React APIs that worked but felt wrong. You had to wrap your entire component in a higher-order function just to pass a ref through. React 19 eliminates it entirely.

// Before — required forwardRef wrapper
import { forwardRef } from 'react'
 
interface InputProps {
  label: string
}
 
const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
  { label },
  ref
) {
  return <input ref={ref} aria-label={label} />
})
// React 19 — ref is just a prop
interface InputProps {
  label: string
  ref?: React.Ref<HTMLInputElement>
}
 
function Input({ label, ref }: InputProps) {
  return <input ref={ref} aria-label={label} />
}
 
// Usage is identical
function Parent() {
  const inputRef = useRef<HTMLInputElement>(null)
 
  return (
    <div>
      <Input label="Email" ref={inputRef} />
      <button onClick={() => inputRef.current?.focus()}>Focus</button>
    </div>
  )
}

Cleaner component signatures, no wrapper, same behavior. forwardRef still works in React 19 (no breaking change), but it's now considered legacy and will likely be deprecated in a future version. New components should use ref as a prop directly.

Cleanup Functions from Refs

React 19 also adds support for cleanup functions from ref callbacks, matching the pattern from useEffect:

function AutoFocusInput() {
  return (
    <input
      ref={(node) => {
        if (node) {
          node.focus()
          // Return a cleanup function
          return () => {
            node.blur()
          }
        }
      }}
    />
  )
}

The cleanup runs when the element unmounts. Previously, React ignored return values from ref callbacks — now they're used for cleanup, which was a common source of memory leak workarounds.

Document Metadata from Components

This one is genuinely nice for co-located code. React 19 supports <title>, <meta>, and <link> tags rendered anywhere in your component tree — React hoists them to <head> automatically.

function BlogPost({ post }: { post: { title: string; description: string; slug: string } }) {
  return (
    <article>
      <title>{post.title} — StackNotice</title>
      <meta name="description" content={post.description} />
      <link rel="canonical" href={`https://stacknotice.com/blog/${post.slug}`} />
 
      <h1>{post.title}</h1>
      <p>{post.description}</p>
      {/* article content */}
    </article>
  )
}

No more react-helmet, no more next/head (for non-Next.js apps), no more hoisting metadata manually. React handles deduplication — if multiple components render <title>, the last one wins (innermost component in the tree takes priority).

If you're using Next.js, the App Router's metadata export is still the recommended approach since it handles more edge cases and integrates with Next's static generation. But for plain React apps or React Router setups, this is a massive quality-of-life improvement.

Server Components and Server Actions (Stable)

React 19 is the version that officially stabilizes the Server Components model. This is significant because frameworks like Next.js have been shipping their own implementations — now there's an official stable spec.

Server Components render on the server and send HTML + a serialized component tree to the client. They can directly access databases, file systems, and secrets without exposing them to the browser. They have zero JavaScript bundle impact on the client.

Server Actions are the async functions that pair with them for mutations:

// app/contact/actions.ts
'use server'
 
import { db } from '@/lib/db'
 
export async function submitContactForm(formData: FormData) {
  const email = formData.get('email') as string
  const message = formData.get('message') as string
 
  if (!email || !message) {
    throw new Error('Email and message are required')
  }
 
  await db.contactSubmissions.create({
    data: { email, message, createdAt: new Date() }
  })
}
// app/contact/page.tsx — Server Component
import { submitContactForm } from './actions'
 
export default function ContactPage() {
  return (
    <form action={submitContactForm}>
      <input name="email" type="email" required />
      <textarea name="message" required />
      <button type="submit">Send Message</button>
    </form>
  )
}

The 'use server' directive marks functions as Server Actions. They can be imported into Client Components and called from event handlers too — they're not limited to form action props.

For a deeper look at when to use Server vs Client Components and the tradeoffs involved, the Next.js Server vs Client Components guide covers the mental model in detail.

Better Error Reporting

React 19 significantly improves the quality of error messages, particularly for hydration mismatches — one of the most confusing categories of React errors.

Previously, a hydration error would tell you that something didn't match between server and client, but give you no indication of what. React 19 shows you the actual diff:

Hydration failed because the server rendered HTML didn't match the client.
  Server: <div class="container" data-theme="dark">
  Client: <div class="container" data-theme="light">

This immediately tells you what's different. In practice, this catches the common pattern of reading localStorage, window, or Date during render without proper guards — things that differ between server and client environments.

React 19 also improves Error Boundary behavior. Errors caught by boundaries now include better stack traces, and onCaughtError / onUncaughtError callbacks give you more granular control over error reporting:

<ErrorBoundary
  fallback={<ErrorFallback />}
  onError={(error, errorInfo) => {
    // Log to your error tracking service
    reportError(error, errorInfo.componentStack)
  }}
>
  <App />
</ErrorBoundary>

How to Upgrade to React 19

Upgrading is straightforward for most projects:

npm install react@19 react-dom@19
# If using TypeScript:
npm install --save-dev @types/react@19 @types/react-dom@19

Breaking Changes to Watch

String refs are removed. If you have ref="inputRef" anywhere (legacy pattern from React 0.x era), it will break. Use useRef instead.

Legacy Context API is deprecated. The contextTypes / childContextTypes pattern is gone. Use createContext + useContext.

ReactDOM.render is removed. You should be on createRoot already, but if not:

// Before (React 17)
ReactDOM.render(<App />, document.getElementById('root'))
 
// React 18+ / 19
const root = ReactDOM.createRoot(document.getElementById('root')!)
root.render(<App />)

propTypes are removed at runtime. PropTypes checking is no longer included in the React package. If you rely on runtime prop type warnings, switch to TypeScript — which you should be using anyway.

Codemods

The React team provides codemods for the most common migrations:

npx codemod react/19/migration-recipe

This handles string refs, legacy context, and several other patterns automatically. Run it before manually reviewing what's left.

Next.js Users

If you're on Next.js 15+, you're already using React 19. The canary channel of React 19 has been the foundation of Next.js App Router since Next.js 13. Upgrading to Next.js 15 effectively upgrades you to stable React 19.

Quick Reference — All New APIs at a Glance

APIPackageWhat it does
useActionStatereactManages state + pending status for async actions
useFormStatusreact-domReads pending state from the nearest parent form
useOptimisticreactShows optimistic state during async operations
use()reactReads promises and context in render (conditionally)
ref as propreactPass refs without forwardRef wrapper
<title> / <meta>react-domMetadata hoisted to <head> from any component
Server ActionsreactAsync server functions with 'use server'
onCaughtErrorreact-domError boundary callback for caught errors
onUncaughtErrorreact-domRoot-level callback for uncaught errors

FAQ

Is React 19 stable?

Yes. React 19 reached stable release in December 2024. It is production-ready and recommended for new projects. The APIs that shipped as stable — useActionState, useOptimistic, use(), ref as prop, document metadata — are not expected to change.

What's the difference between useActionState and useState?

useState gives you a state value and a setter. You manage transitions, loading states, and error handling yourself. useActionState takes an async function (your action), wraps it with transition semantics, and returns [state, dispatchAction, isPending] — the pending boolean and state updates are handled automatically. It's specifically designed for async operations triggered by user actions, not general-purpose state.

Do I need Server Components to use React 19?

No. Every feature covered in this article except Server Components and Server Actions works in a standard client-side React 19 app. useActionState, useOptimistic, use(), ref as prop, and document metadata all work without a server rendering environment. Server Components are opt-in and require framework support (Next.js App Router, Remix, etc.).

Is forwardRef removed in React 19?

No, forwardRef is not removed — it still works. It's now considered legacy and will eventually be deprecated in a future major version. New code should pass ref as a regular prop instead. Existing components using forwardRef don't need to be migrated immediately.

Can I use useOptimistic outside of Server Actions?

Yes. useOptimistic works with any async operation — regular API calls, mutations to a local database, anything. It's not tied to Server Actions or form submissions. You call addOptimisticUpdate before your async operation and React handles showing the optimistic state until the real state arrives.

React 19 + The Compiler: The Full Picture

React 19's new APIs reduce the code you write for async patterns. But there's a parallel change that eliminates a different category of boilerplate entirely: the React Compiler, which reached stable 1.0 in late 2025.

The Compiler automatically memoizes components, values, and callbacks — making useMemo, useCallback, and memo() largely unnecessary. Combined with React 19's Actions and useOptimistic, you end up writing significantly less ceremony code overall.

For the full breakdown of what the Compiler does and whether useMemo is actually dead, read React Compiler Is Here — Is useMemo Dead?.

React 19 is the version where React stopped feeling like it was fighting you and started feeling like it was working with you. The patterns are cleaner, the APIs are more cohesive, and the error messages actually help. If you haven't upgraded yet, now's the time.

#react#react-19#hooks#server-components#javascript
Share:

Enjoyed this article?

Join 2,400+ developers getting weekly insights on Claude Code, React, and AI tools.

No spam. Unsubscribe anytime. By subscribing you agree to our Privacy Policy.