React 19: New Features, Breaking Changes, and Code Examples

Lokman Musliu Founder and CEO of Lucky Media
Lokman Musliu

July 15, 2024 Β· 5 min read

React 19 release date

React 19 stable was released on December 5, 2024 - the biggest API surface change since hooks arrived in React 16.8. This guide covers every new feature that shipped: the four new hooks (useActionState, useOptimistic, useFormStatus, use()), stable Server Components and Server Actions, the React Compiler, forwardRef deprecation, document metadata in components, and all breaking changes to handle before upgrading.

React 19 is now stable

React 19 reached stable release on December 5, 2024. To upgrade an existing project:

npm install react@19 react-dom@19

For TypeScript projects, also update the type definitions:

npm install @types/react@19 @types/react-dom@19

The new hooks

useActionState

useActionState replaces the old useFormState pattern. It wraps an async action and gives you pending state, error state, and the result - all managed automatically. This removes the boilerplate of manually tracking loading and error state for form submissions and mutations.

import { useActionState } from 'react'

async function updateUsername(previousState, formData) {
  const username = formData.get('username')
  try {
    await saveUsername(username)
    return { success: true }
  } catch (error) {
    return { error: 'Username already taken.' }
  }
}

function UsernameForm() {
  const [state, formAction, isPending] = useActionState(updateUsername, null)

  return (
    <form action={formAction}>
      <input name="username" />
      <button disabled={isPending}>
        {isPending ? 'Saving...' : 'Save'}
      </button>
      {state?.error && <p>{state.error}</p>}
    </form>
  )
}

useOptimistic

useOptimistic lets you show the expected result of an action immediately while the server request is still in flight. If the request fails, React automatically reverts to the previous state. This makes interactions feel instant without any manual rollback logic.

import { useOptimistic } from 'react'

function LikeButton({ postId, initialLikes }) {
  const [optimisticLikes, addOptimisticLike] = useOptimistic(
    initialLikes,
    (current) => current + 1
  )

  async function handleLike() {
    addOptimisticLike()
    await likePost(postId)
  }

  return (
    <button onClick={handleLike}>
      {optimisticLikes} Likes
    </button>
  )
}

useFormStatus

useFormStatus reads the pending state of the parent <form> element from any child component, without prop drilling. This is particularly useful for submit buttons and input fields that need to be disabled during form submission.

import { useFormStatus } from 'react-dom'

function SubmitButton() {
  const { pending } = useFormStatus()

  return (
    <button disabled={pending}>
      {pending ? 'Submitting...' : 'Submit'}
    </button>
  )
}

function ContactForm() {
  return (
    <form action={submitForm}>
      <input name="email" type="email" />
      <SubmitButton />
    </form>
  )
}

use()

The use() hook reads the value of a Promise or Context during render. Unlike all other hooks, use() can be called conditionally, inside if statements or loops. When called with a Promise, it suspends the component until the Promise resolves.

import { use, Suspense } from 'react'

function UserProfile({ userPromise }) {
  // Suspends until the promise resolves
  const user = use(userPromise)
  return <h1>{user.name}</h1>
}

// Conditional context reading
function ThemeButton({ showTheme }) {
  if (showTheme) {
    // use() can be called conditionally β€” other hooks cannot
    const theme = use(ThemeContext)
    return <button style={{ background: theme.primary }}>Click</button>
  }
  return <button>Click</button>
}

Server Components and Server Actions

Server Components and Server Actions are stable in React 19. Server Components run only on the server, they have no JavaScript footprint on the client, can access databases and file systems directly, and reduce the bundle size sent to the browser.

Server Actions use the "use server" directive to mark functions that run on the server but can be called from Client Components. They work seamlessly with useActionState for end-to-end form handling:

// actions.js β€” Server Action
'use server'

export async function createPost(previousState, formData) {
  const title = formData.get('title')
  const content = formData.get('content')

  try {
    await db.posts.create({ title, content })
    return { success: true }
  } catch (error) {
    return { error: 'Failed to create post.' }
  }
}
// NewPostForm.jsx β€” Client Component calling a Server Action
'use client'

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

export function NewPostForm() {
  const [state, formAction, isPending] = useActionState(createPost, null)

  return (
    <form action={formAction}>
      <input name="title" placeholder="Title" />
      <textarea name="content" placeholder="Content" />
      <button disabled={isPending}>
        {isPending ? 'Creating...' : 'Create Post'}
      </button>
      {state?.error && <p>{state.error}</p>}
    </form>
  )
}

React Compiler

The React Compiler analyzes your component code statically at build time and inserts memoization automatically. It removes the need to manually write useMemo, useCallback, and React.memo wrappers in components that have unnecessary re-renders.

Performance gains vary by application, components that were previously poorly memoized will see significant improvements, while components already well-optimized will see minimal change. The React Compiler is available as babel-plugin-react-compiler and is built into Next.js 15 and 16 via the reactCompiler: true config option.

npm install babel-plugin-react-compiler@latest

ref as a prop, forwardRef is deprecated

In React 19, function components receive ref as a regular prop. The forwardRef wrapper is deprecated and will show a deprecation warning. Existing forwardRef usage still works in React 19 but should be migrated.

// Before React 19 β€” forwardRef required
const TextInput = forwardRef(({ placeholder }, ref) => (
  <input ref={ref} placeholder={placeholder} />
))

// React 19 β€” ref is a regular prop
const TextInput = ({ placeholder, ref }) => (
  <input ref={ref} placeholder={placeholder} />
)

// Usage is identical in both cases
const inputRef = useRef(null)
<TextInput ref={inputRef} placeholder="Enter text" />

Document metadata in components

React 19 natively supports rendering <title>, <meta>, and <link> tags from inside any component. React automatically hoists them to the <head> of the document and handles deduplication, multiple components declaring the same <title> will result in only one entry in the <head>.

function BlogPost({ post }) {
  return (
    <article>
      <title>{post.title}</title>
      <meta name="description" content={post.excerpt} />
      <link rel="canonical" href={post.url} />
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </article>
  )
}

Stylesheet and resource management

React 19 adds built-in support for controlling stylesheet loading order and deduplicating async scripts. Use the precedence attribute on <link rel="stylesheet"> to specify load order relative to other stylesheets. React ensures each stylesheet is loaded only once regardless of how many components reference it.

function Component() {
  return (
    <div>
      {/* precedence controls load order */}
      <link rel="stylesheet" href="/base.css" precedence="default" />
      <link rel="stylesheet" href="/theme.css" precedence="high" />
      {/* Async scripts are deduplicated automatically */}
      <script async src="/analytics.js" />
    </div>
  )
}

React 19 also introduces low-level resource preloading APIs for performance optimization: prefetchDNS(), preconnect(), preload(), and preinit(). These can be called from event handlers to kick off resource loading before the user navigates.

Custom Elements (Web Components) support

React 19 passes all Web Components tests in the Custom Elements Everywhere suite. Custom element props are now correctly handled: non-string props are set as properties (not attributes) by default, matching how native HTML elements behave. This makes it straightforward to use any Web Component library alongside React without adapters or wrappers.

Improved error handling and hydration

React 19 consolidates duplicate error messages in development. Previously, a single error could trigger multiple console logs from different parts of the error boundary system. Now you see one clean, complete error message.

Hydration mismatch errors now show a diff between the server-rendered HTML and the client-rendered output, making it significantly easier to identify what caused the mismatch. The new onCaughtError, onUncaughtError, and onRecoverableError callbacks on createRoot give you fine-grained control over error reporting for each category.

How to upgrade to React 19

Install React 19 and update TypeScript types:

npm install react@19 react-dom@19
npm install --save-dev @types/react@19 @types/react-dom@19

Key breaking changes to address:

  • propTypes removed from components: use TypeScript instead

  • String ref (legacy ref="input") removed: use useRef

  • ReactDOM.render removed: use createRoot

  • ReactDOM.hydrate removed: use hydrateRoot

  • useFormState removed: replace with useActionState

For Next.js users: Next.js 15 and Next.js 16 both include React 19 support. If you are on Next.js 15 or 16, React 19 is already included, see Next.js 16: What's New, Key Features & How to Upgrade for the full Next.js upgrade guide.


Bring Your Ideas to Life πŸš€

If you need help with a React project let's get in touch.

Lucky Media is proud to be recognized as a leading Next.js Development Agency

FAQs

Is React 19 stable?

Yes. React 19 reached stable release on December 5, 2024. It is production-ready and safe to use in new and existing projects.

What replaces forwardRef in React 19?

Nothing replaces it, forwardRef is no longer needed. In React 19, ref is a regular prop on function components. You can access it directly as ({ ref, ...props }) in your component definition. The forwardRef wrapper is deprecated and will show a warning, but still works in React 19.

What is useActionState and how is it different from useFormState?

useActionState is the React 19 replacement for useFormState (which is removed in React 19). It wraps an async action function and returns the current state, the action to pass to a form, and a pending boolean. The key improvement is that it also exposes an isPending value directly, and integrates cleanly with Server Actions.

Do I need to update my code to upgrade to React 19?

Yes, React 19 removes several deprecated APIs: ReactDOM.render, ReactDOM.hydrate, string refs, propTypes, and useFormState. The React team provides a codemod to handle the most common migrations automatically. Check the official React 19 upgrade guide for the complete list of breaking changes.

Can use() replace useEffect for data fetching?

The use() hook reads the value of a Promise during render and suspends the component until it resolves, it is not a replacement for useEffect. Use it when you already have a Promise (passed as a prop or created outside the component) and want to consume it in render. For most data fetching, pairing use() with a data-fetching library (like SWR or React Query) or a Server Component is the recommended approach.

Technologies

React
Lokman Musliu Founder and CEO of Lucky Media
Lokman Musliu

Founder and CEO of Lucky Media

Stay up-to-date

Be updated with all news, products and tips we share!

Let’s chat

We partner with a limited number of brands each quarter to ensure senior-level attention on every project.

lokman and arlind headshots
Teamwork