ReactReact 19 New Features

React 19 New Features

React 19 (released December 2024) is the most feature-rich React release since Hooks in 16.8. It introduces the stable Actions API, new built-in hooks for async mutations and optimistic UI, the React Compiler for automatic memoisation, and several quality-of-life improvements that eliminate boilerplate that previously required dozens of lines. This page covers each feature with practical examples.

The `use()` Hook

use() is a new hook that lets you read a resource — a Promise or a Context — during render, without useEffect. When passed a Promise, React suspends the component until the Promise resolves, integrating naturally with Suspense.

TSX
'use client'
import { use, Suspense } from 'react'

// Create the promise outside the component so it isn't recreated each render
const userPromise = fetch('/api/user').then((r) => r.json())

function UserProfile() {
  // React suspends here until the promise resolves — no useEffect!
  const user = use(userPromise)
  return <p>Hello, {user.name}</p>
}

export default function App() {
  return (
    <Suspense fallback={<p>Loading user…</p>}>
      <UserProfile />
    </Suspense>
  )
}

use() can also read Context and — unlike useContext — can be called conditionally inside loops and if-statements, because it is not bound by the rules of hooks in the same way:

TSX
'use client'
import { use } from 'react'
import { ThemeContext } from './ThemeContext'

function Button({ showIcon }: { showIcon: boolean }) {
  // Reading context with use() — works inside conditions
  if (showIcon) {
    const theme = use(ThemeContext)
    return <button style={{ background: theme.primary }}>★ Submit</button>
  }
  return <button>Submit</button>
}
Server Actions

Server Actions are async functions marked with 'use server' that run exclusively on the server. They can be called directly from form action attributes or event handlers — React serialises the call over the network automatically. No API route boilerplate required.

TSX
// actions.ts
'use server'
import { db } from '@/lib/db'
import { revalidatePath } from 'next/cache'

export async function createPost(formData: FormData) {
  const title = formData.get('title') as string
  const body = formData.get('body') as string

  await db.post.create({ data: { title, body } })
  revalidatePath('/posts') // bust the Next.js cache for /posts
}

// CreatePostForm.tsx
import { createPost } from './actions'

export function CreatePostForm() {
  return (
    // action={serverAction} works even without JavaScript in the browser
    <form action={createPost}>
      <input name="title" placeholder="Title" required />
      <textarea name="body" placeholder="Body" />
      <button type="submit">Publish</button>
    </form>
  )
}
`useActionState` (formerly `useFormState`)

useActionState wraps a server action and gives you its pending state and the last returned value — replacing the old useFormState from react-dom:

TSX
'use client'
import { useActionState } from 'react'
import { createPost } from './actions'

type State = { error?: string; success?: boolean }

export function CreatePostForm() {
  const [state, formAction, isPending] = useActionState<State, FormData>(
    async (prevState, formData) => {
      try {
        await createPost(formData)
        return { success: true }
      } catch (e) {
        return { error: 'Failed to create post' }
      }
    },
    {} // initial state
  )

  return (
    <form action={formAction}>
      <input name="title" placeholder="Title" required />
      {state.error && <p style={{ color: 'red' }}>{state.error}</p>}
      {state.success && <p style={{ color: 'green' }}>Post created!</p>}
      <button type="submit" disabled={isPending}>
        {isPending ? 'Publishing…' : 'Publish'}
      </button>
    </form>
  )
}
`useOptimistic` — Built-in Optimistic UI

useOptimistic lets you show an optimistic (immediate) update while the server action is in flight, then reconcile with the real response. Previously this required complex manual state juggling:

TSX
'use client'
import { useOptimistic, useTransition } from 'react'
import { toggleLike } from './actions'

interface Props {
  postId: string
  initialLiked: boolean
  initialCount: number
}

export function LikeButton({ postId, initialLiked, initialCount }: Props) {
  const [isPending, startTransition] = useTransition()
  const [optimisticState, setOptimistic] = useOptimistic(
    { liked: initialLiked, count: initialCount },
    (current, newLiked: boolean) => ({
      liked: newLiked,
      count: current.count + (newLiked ? 1 : -1),
    })
  )

  function handleClick() {
    startTransition(async () => {
      const newLiked = !optimisticState.liked
      setOptimistic(newLiked)   // instant UI update
      await toggleLike(postId, newLiked) // actual server call
    })
  }

  return (
    <button onClick={handleClick} disabled={isPending}>
      {optimisticState.liked ? '❤️' : '🤍'} {optimisticState.count}
    </button>
  )
}
`useFormStatus`

useFormStatus (from react-dom) reads the pending state of the nearest parent <form> submission. It is ideal for building submit buttons that are decoupled from the form they live in:

TSX
'use client'
import { useFormStatus } from 'react-dom'

// This component doesn't need to know anything about the form
export function SubmitButton() {
  const { pending } = useFormStatus()

  return (
    <button type="submit" disabled={pending}>
      {pending ? 'Saving…' : 'Save'}
    </button>
  )
}

// Use it anywhere inside a <form>
import { updateProfile } from './actions'

export function ProfileForm() {
  return (
    <form action={updateProfile}>
      <input name="displayName" />
      <SubmitButton />
    </form>
  )
}
Ref as Prop (No More `forwardRef`)

React 19 allows function components to receive ref as a regular prop. The old forwardRef wrapper is no longer needed for new code:

TSX
// React 19 — ref is just a prop
interface InputProps {
  label: string
  ref?: React.Ref<HTMLInputElement>
}

export function LabelledInput({ label, ref, ...props }: InputProps) {
  return (
    <label>
      {label}
      <input ref={ref} {...props} />
    </label>
  )
}

// Usage — no forwardRef wrapper needed
function Form() {
  const inputRef = React.useRef<HTMLInputElement>(null)

  return (
    <form>
      <LabelledInput label="Name" ref={inputRef} name="name" />
      <button type="button" onClick={() => inputRef.current?.focus()}>
        Focus input
      </button>
    </form>
  )
}
`<Context>` as Provider

You can now render a Context object directly as a JSX element instead of using Context.Provider. It's a small quality-of-life improvement that removes visual noise:

TSX
import { createContext } from 'react'

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

// React 19 — render the context itself as the provider
export function App() {
  return (
    <ThemeContext value="dark">
      <Layout />
    </ThemeContext>
  )
}

// React 18 and below — required .Provider
export function AppLegacy() {
  return (
    <ThemeContext.Provider value="dark">
      <Layout />
    </ThemeContext.Provider>
  )
}
Note
`Context.Provider` still works in React 19 for backward compatibility. Migrate at your own pace — the old syntax will be deprecated in a future release.
The React Compiler

The React Compiler (formerly "React Forget") is an opt-in Babel/SWC transform that automatically inserts useMemo, useCallback, and React.memo where the compiler can prove they are safe. The goal: write plain React code without manually reasoning about memoisation.

Bash
# Install the compiler for Next.js
npm install --save-dev babel-plugin-react-compiler

JS
// next.config.mjs
/** @type {import('next').NextConfig} */
const nextConfig = {
  experimental: {
    reactCompiler: true,
  },
}

export default nextConfig

With the compiler enabled, this component is automatically memoised — no useMemo needed:

TSX
// Before React Compiler — manual memoisation
function ExpensiveList({ items, onSelect }: Props) {
  const sorted = useMemo(() => [...items].sort(), [items])
  const handleSelect = useCallback((id: string) => onSelect(id), [onSelect])
  return <ul>{sorted.map((item) => <li key={item.id} onClick={() => handleSelect(item.id)}>{item.name}</li>)}</ul>
}

// After React Compiler — compiler does it automatically
function ExpensiveList({ items, onSelect }: Props) {
  const sorted = [...items].sort()
  return <ul>{sorted.map((item) => <li key={item.id} onClick={() => onSelect(item.id)}>{item.name}</li>)}</ul>
}
When to Upgrade from React 18
  • You're starting a new project — use React 19 from day one

  • You use Next.js 15 — it defaults to React 19

  • You want Server Actions or useOptimistic without workarounds

  • You have many forwardRef wrappers you'd like to simplify

  • You want to opt in to the React Compiler

Warning
React 19 removes several long-deprecated APIs: `propTypes` on components, `defaultProps` on function components, string refs, and the legacy `ReactDOM.render()`. Run the official React 19 codemod (`npx react-codemod`) before upgrading a large existing codebase.
Check the upgrade guide
The official React 19 upgrade guide at react.dev covers every breaking change and provides codemods for most of them. The migration from React 18 is straightforward for most apps — typically a few hours of work.