React 19 New Features Guide 2026 — Actions, useOptimistic, use() and More
Advertisement
Introduction
Why This Matters
React 19 ships the most significant API changes since hooks arrived in 16.8. It solves recurring pain points — manual loading states, optimistic updates, async data reading — with purpose-built primitives that reduce boilerplate by 50–70% in real applications.
Actions and useActionState
Before React 19, every form needed manual isPending, error, and try/catch state. Actions collapse this into one hook:
import { useActionState } from 'react'
// React 18 pattern (verbose):
function OldForm() {
const [error, setError] = useState(null)
const [isPending, setIsPending] = useState(false)
async function handleSubmit(e) {
e.preventDefault()
setIsPending(true)
try {
await submitForm(new FormData(e.target))
} catch (err) {
setError(err.message)
} finally {
setIsPending(false)
}
}
return <form onSubmit={handleSubmit}>...</form>
}
// React 19 pattern (concise):
function NewForm() {
const [state, formAction, isPending] = useActionState(
async (prevState: any, formData: FormData) => {
try {
await submitForm(formData)
return { success: true, error: null }
} catch (err) {
return { success: false, error: err.message }
}
},
{ success: false, error: null }
)
return (
<form action={formAction}>
{state.error && <p className="text-red-500">{state.error}</p>}
<input name="email" type="email" required />
<button disabled={isPending}>
{isPending ? 'Submitting…' : 'Submit'}
</button>
</form>
)
}useOptimistic: Instant UI Feedback
useOptimistic applies a temporary state change immediately, then reverts or confirms when the server responds:
'use client'
import { useOptimistic, useActionState } from 'react'
interface Message {
id: string
text: string
pending?: boolean
}
function MessageList({ initialMessages }: { initialMessages: Message[] }) {
const [optimisticMessages, addOptimistic] = useOptimistic(
initialMessages,
(state: Message[], newMessage: Message) => [...state, newMessage]
)
const [, formAction] = useActionState(
async (_: any, formData: FormData) => {
const text = formData.get('text') as string
addOptimistic({ id: crypto.randomUUID(), text, pending: true })
await sendMessage(text)
},
null
)
return (
<>
<ul>
{optimisticMessages.map(msg => (
<li key={msg.id} className={msg.pending ? 'opacity-50' : 'opacity-100'}>
{msg.text} {msg.pending && '(sending…)'}
</li>
))}
</ul>
<form action={formAction}>
<input name="text" placeholder="Type a message" />
<button type="submit">Send</button>
</form>
</>
)
}The use() Hook
use() reads a Promise or Context during render, suspending automatically until resolved:
import { use, Suspense } from 'react'
function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
const user = use(userPromise) // suspends until resolved
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
)
}
function Page() {
const userPromise = fetchUser(1) // created outside render
return (
<Suspense fallback={<Skeleton />}>
<UserProfile userPromise={userPromise} />
</Suspense>
)
}
// use() also works for Context — and can be called conditionally
function ThemedButton() {
const theme = use(ThemeContext)
return <button className={theme.button}>Click me</button>
}ref as a Prop
forwardRef is no longer needed — ref is now a standard prop:
// React 19: ref is just a prop
function Input({ ref, ...props }: React.ComponentProps<'input'>) {
return <input ref={ref} {...props} className="border rounded px-3 py-2" />
}
function Form() {
const inputRef = useRef<HTMLInputElement>(null)
return (
<form onSubmit={() => inputRef.current?.focus()}>
<Input ref={inputRef} type="text" name="username" />
<button type="submit">Submit</button>
</form>
)
}Document Metadata Hoisting
Title, meta, and link tags placed inside components are automatically hoisted to <head>:
function BlogPost({ post }: { post: Post }) {
return (
<article>
<title>{post.title} | My Blog</title>
<meta name="description" content={post.excerpt} />
<link rel="canonical" href={`https://example.com/blog/${post.slug}`} />
<h1>{post.title}</h1>
<div>{post.content}</div>
</article>
)
}Common Mistakes
- Creating the Promise passed to
use()inside the component — this re-creates it on every render - Using
useEffect+useStatefor async data whenuse()+ Suspense is simpler - Forgetting to wrap
use()consumers in a<Suspense>boundary - Keeping
forwardRefwrappers after upgrading — they still work but add dead code - Not providing an initial state to
useActionStatethat matches the returned shape
Best Practices
- Use
useActionStatefor all form submissions — it handles pending, error, and success in one hook - Pair
useOptimisticwithuseActionStatefor chat, likes, or any list mutation - Create Promises at the page or layout level, then pass them to child components for
use() - Use
useTransitionwith async functions when you need to control pending state outside a form - Replace
Context.ProviderwithContextdirectly (React 19 supports it)
Key Takeaways
useActionStatereplaces manualisPending+errorstate for async form submissionsuseOptimisticapplies an immediate optimistic state and reverts it if the server call failsuse()suspends a component while a Promise resolves, eliminatinguseEffectdata-fetching patternsuse()can be called conditionally unlike any other hook- React 19 accepts
refas a plain prop —forwardRefis no longer needed for new components title,meta, andlinktags inside components are automatically hoisted to<head>useTransitionnow accepts async functions directly in React 19- Three new root-level error callbacks (
onCaughtError,onUncaughtError,onRecoverableError) improve observability
Advertisement