React 19 New Features Guide 2026: Actions, useOptimistic, use() and More
Advertisement
React 19: The Biggest Update Since Hooks
React 19 ships with the most significant API changes since React 16.8 introduced hooks. These features solve long-standing pain points: form handling, optimistic updates, async state, and more.
- Actions: Async Transitions
- useOptimistic: Instant UI Updates
- The use() Hook: Read Resources in Render
- ref as a Prop (Goodbye forwardRef)
- Document Metadata in Components
- Improved Error Handling
- Context Without Provider
- useTransition with Async
- Migration from React 18
- React 19 vs React 18 Feature Comparison
Actions: Async Transitions
React 19 introduces Actions — async functions that handle transitions automatically:
import { useActionState, useOptimistic } from 'react'
// OLD WAY (React 18):
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>
}
// NEW WAY (React 19):
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" />
<button disabled={isPending}>
{isPending ? 'Submitting...' : 'Submit'}
</button>
</form>
)
}
useOptimistic: Instant UI Updates
'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
const optimisticMsg = { id: crypto.randomUUID(), text, pending: true }
// Show optimistically immediately
addOptimistic(optimisticMsg)
// Then send to server
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: Read Resources in Render
import { use, Suspense } from 'react'
// use() can read Promises and Context
function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
// Suspends until promise resolves
const user = use(userPromise)
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
)
}
// Parent creates the promise and passes it down
function Page() {
// Promise created OUTSIDE render (not in component)
const userPromise = fetchUser(1)
return (
<Suspense fallback={<Skeleton />}>
<UserProfile userPromise={userPromise} />
</Suspense>
)
}
// use() also replaces useContext() for conditional reads
function ThemedButton() {
const theme = use(ThemeContext) // Can be used conditionally!
return <button className={theme.button}>Click me</button>
}
ref as a Prop (Goodbye forwardRef)
// React 19: ref is just a prop now
function Input({ ref, ...props }: React.ComponentProps<'input'>) {
return <input ref={ref} {...props} className="border rounded px-3 py-2" />
}
// Usage
function Form() {
const inputRef = useRef<HTMLInputElement>(null)
return (
<form onSubmit={() => inputRef.current?.focus()}>
<Input ref={inputRef} type="text" />
</form>
)
}
// forwardRef still works for backward compat, but not needed
Document Metadata in Components
// React 19: Hoist title, meta, link tags to <head> automatically
function BlogPost({ post }: { post: Post }) {
return (
<article>
{/* These get hoisted to <head> */}
<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>
<p>{post.content}</p>
</article>
)
}
Improved Error Handling
// React 19 adds onCaughtError, onUncaughtError, onRecoverableError
import { createRoot } from 'react-dom/client'
const root = createRoot(document.getElementById('root')!, {
onCaughtError(error, errorInfo) {
// Error caught by error boundary
logToSentry(error, { extra: errorInfo.componentStack })
},
onUncaughtError(error, errorInfo) {
// Error not caught by error boundary
reportCriticalError(error)
},
onRecoverableError(error) {
// Hydration errors that React auto-recovered from
console.warn('Recoverable error:', error)
},
})
root.render(<App />)
Context Without Provider
// React 19: Context.Provider is now just Context
const ThemeContext = createContext('light')
// Before:
function App() {
return (
<ThemeContext.Provider value="dark">
<Page />
</ThemeContext.Provider>
)
}
// After (React 19):
function App() {
return (
<ThemeContext value="dark">
<Page />
</ThemeContext>
)
}
useTransition with Async
import { useTransition } from 'react'
function SearchPage() {
const [query, setQuery] = useState('')
const [results, setResults] = useState([])
const [isPending, startTransition] = useTransition()
function handleSearch(e: React.ChangeEvent<HTMLInputElement>) {
const value = e.target.value
setQuery(value)
// React 19: startTransition accepts async functions
startTransition(async () => {
const data = await searchAPI(value)
setResults(data)
})
}
return (
<div>
<input value={query} onChange={handleSearch} placeholder="Search..." />
{isPending ? (
<div className="animate-pulse">Searching...</div>
) : (
<ResultsList results={results} />
)}
</div>
)
}
Migration from React 18
// 1. Update packages
// npm install react@19 react-dom@19
// 2. Replace forwardRef with ref prop
// Before:
const Button = forwardRef<HTMLButtonElement, ButtonProps>((props, ref) => (
<button ref={ref} {...props} />
))
// After:
const Button = ({ ref, ...props }: ButtonProps & { ref?: React.Ref<HTMLButtonElement> }) => (
<button ref={ref} {...props} />
)
// 3. Replace useDeferredValue for async → useTransition
// 4. Replace manual loading states with useActionState
// 5. Replace optimistic updates with useOptimistic
React 19 vs React 18 Feature Comparison
| Feature | React 18 | React 19 |
|---|---|---|
| Form handling | Manual state | useActionState |
| Optimistic UI | Manual | useOptimistic |
| Async data | useEffect | use() + Suspense |
| Ref forwarding | forwardRef | Ref as prop |
| Context | Context.Provider | Context directly |
| Error tracking | Limited | 3 new callbacks |
React 19 dramatically reduces boilerplate for the most common patterns. Start using useActionState and useOptimistic on your next form — you'll never go back.
Advertisement