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.
'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:
'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.
// 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:
'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:
'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:
'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:
// 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:
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>
)
}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.
# Install the compiler for Next.js npm install --save-dev babel-plugin-react-compiler
// next.config.mjs
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
reactCompiler: true,
},
}
export default nextConfigWith the compiler enabled, this component is automatically memoised — no useMemo needed:
// 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
useOptimisticwithout workaroundsYou have many
forwardRefwrappers you'd like to simplifyYou want to opt in to the React Compiler