React 19 has officially released with many new features including Server Actions, new hooks, and significant improvements to form handling. This article explains React 19’s major new features with practical code examples.
React 19 Main New Features
Overview
flowchart TB
subgraph React19["React 19"]
subgraph Hooks["New Hooks"]
H1["use() - Read Promise/Context"]
H2["useActionState() - Form action state"]
H3["useFormStatus() - Form submission state"]
H4["useOptimistic() - Optimistic updates"]
end
subgraph Actions["Actions"]
A1["Server Actions - Server-side functions"]
A2["Client Actions - Client async processing"]
A3["Form Integration - form action"]
end
subgraph Other["Other Improvements"]
O1["ref as prop - No forwardRef needed"]
O2["Document Metadata - Direct title etc."]
O3["Stylesheet management - Order via precedence"]
O4["Resource Preloading - prefetch/preload API"]
end
end
use() Hook
Reading Promises
use() is a new hook for reading Promises or Context during rendering.
// use() - Reading Promises
import { use, Suspense } from 'react';
// Data fetch function
async function fetchUser(id: string): Promise<User> {
const response = await fetch(`/api/users/${id}`);
return response.json();
}
// Component
function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
// Use with Suspense
const user = use(userPromise);
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}
// Parent component
function UserPage({ userId }: { userId: string }) {
// Pass Promise as props
const userPromise = fetchUser(userId);
return (
<Suspense fallback={<div>Loading user...</div>}>
<UserProfile userPromise={userPromise} />
</Suspense>
);
}
// Can call use() conditionally (unlike other hooks)
function ConditionalData({ shouldFetch, dataPromise }: {
shouldFetch: boolean;
dataPromise: Promise<Data>;
}) {
if (!shouldFetch) {
return <div>No data needed</div>;
}
// Can use after conditional branch
const data = use(dataPromise);
return <div>{data.value}</div>;
}
Reading Context
// use() - Reading Context
import { use, createContext } from 'react';
const ThemeContext = createContext<'light' | 'dark'>('light');
function ThemedButton() {
// Can use use() instead of useContext()
const theme = use(ThemeContext);
return (
<button className={theme === 'dark' ? 'bg-gray-800' : 'bg-white'}>
Click me
</button>
);
}
// Conditional Context reading
function ConditionalTheme({ useTheme }: { useTheme: boolean }) {
if (!useTheme) {
return <button>Default Button</button>;
}
// Can use after conditional branch
const theme = use(ThemeContext);
return <button className={`theme-${theme}`}>Themed Button</button>;
}
Actions
Server Actions
// app/actions.ts
'use server';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
// Server Action
export async function createPost(formData: FormData) {
const title = formData.get('title') as string;
const content = formData.get('content') as string;
// Validation
if (!title || title.length < 3) {
return { error: 'Title must be at least 3 characters' };
}
// Save to database
const post = await db.post.create({
data: { title, content },
});
// Revalidate cache
revalidatePath('/posts');
// Redirect
redirect(`/posts/${post.id}`);
}
// Update action
export async function updatePost(id: string, formData: FormData) {
const title = formData.get('title') as string;
const content = formData.get('content') as string;
await db.post.update({
where: { id },
data: { title, content },
});
revalidatePath(`/posts/${id}`);
return { success: true };
}
// Delete action
export async function deletePost(id: string) {
await db.post.delete({ where: { id } });
revalidatePath('/posts');
redirect('/posts');
}
Form Integration
// app/posts/new/page.tsx
'use client';
import { useActionState } from 'react';
import { createPost } from '../actions';
export default function NewPostPage() {
// useActionState - Form action state management
const [state, formAction, isPending] = useActionState(
createPost,
{ error: null }
);
return (
<form action={formAction}>
<div>
<label htmlFor="title">Title</label>
<input
id="title"
name="title"
required
disabled={isPending}
/>
</div>
<div>
<label htmlFor="content">Content</label>
<textarea
id="content"
name="content"
disabled={isPending}
/>
</div>
{state?.error && (
<p className="text-red-500">{state.error}</p>
)}
<button type="submit" disabled={isPending}>
{isPending ? 'Posting...' : 'Post'}
</button>
</form>
);
}
useFormStatus
// Get form submission status
import { useFormStatus } from 'react-dom';
function SubmitButton() {
// Get parent <form>'s submission status
const { pending, data, method, action } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? (
<>
<Spinner />
Submitting...
</>
) : (
'Submit'
)}
</button>
);
}
// Use in form
function ContactForm() {
async function submitForm(formData: FormData) {
'use server';
// Submit processing
}
return (
<form action={submitForm}>
<input name="email" type="email" required />
<textarea name="message" required />
{/* SubmitButton automatically gets parent form's status */}
<SubmitButton />
</form>
);
}
useOptimistic - Optimistic Updates
// Implementing optimistic updates
import { useOptimistic, startTransition } from 'react';
interface Message {
id: string;
text: string;
sending?: boolean;
}
function ChatMessages({ messages }: { messages: Message[] }) {
// Optimistic state management
const [optimisticMessages, addOptimisticMessage] = useOptimistic(
messages,
(state, newMessage: Message) => [
...state,
{ ...newMessage, sending: true },
]
);
async function sendMessage(formData: FormData) {
const text = formData.get('message') as string;
const tempId = crypto.randomUUID();
// Add optimistically
startTransition(() => {
addOptimisticMessage({
id: tempId,
text,
sending: true,
});
});
// Send to server
await fetch('/api/messages', {
method: 'POST',
body: JSON.stringify({ text }),
});
}
return (
<div>
<ul>
{optimisticMessages.map((message) => (
<li
key={message.id}
className={message.sending ? 'opacity-50' : ''}
>
{message.text}
{message.sending && <span> (Sending...)</span>}
</li>
))}
</ul>
<form action={sendMessage}>
<input name="message" required />
<button type="submit">Send</button>
</form>
</div>
);
}
Like Button Example
// Optimistic like button
function LikeButton({ postId, initialLiked, initialCount }: {
postId: string;
initialLiked: boolean;
initialCount: number;
}) {
const [{ liked, count }, setOptimistic] = useOptimistic(
{ liked: initialLiked, count: initialCount },
(state, newLiked: boolean) => ({
liked: newLiked,
count: state.count + (newLiked ? 1 : -1),
})
);
async function toggleLike() {
const newLiked = !liked;
// Update optimistically
startTransition(() => {
setOptimistic(newLiked);
});
// Send to server
await fetch(`/api/posts/${postId}/like`, {
method: newLiked ? 'POST' : 'DELETE',
});
}
return (
<button onClick={toggleLike}>
{liked ? '❤️' : '🤍'} {count}
</button>
);
}
ref as prop
In React 19, ref can be received as props without forwardRef.
// Before React 18: forwardRef required
const InputOld = forwardRef<HTMLInputElement, InputProps>((props, ref) => {
return <input ref={ref} {...props} />;
});
// React 19: ref can be passed as normal prop
function Input({ ref, ...props }: InputProps & { ref?: React.Ref<HTMLInputElement> }) {
return <input ref={ref} {...props} />;
}
// Usage
function Form() {
const inputRef = useRef<HTMLInputElement>(null);
return (
<form>
<Input ref={inputRef} placeholder="Enter text" />
<button
type="button"
onClick={() => inputRef.current?.focus()}
>
Focus
</button>
</form>
);
}
Document Metadata
Metadata can now be written directly within components.
// Direct metadata writing
function BlogPost({ post }: { post: Post }) {
return (
<article>
{/* Automatically hoisted to document head */}
<title>{post.title} - My Blog</title>
<meta name="description" content={post.excerpt} />
<meta property="og:title" content={post.title} />
<meta property="og:description" content={post.excerpt} />
<link rel="canonical" href={`https://myblog.com/posts/${post.slug}`} />
<h1>{post.title}</h1>
<div>{post.content}</div>
</article>
);
}
// Usage across multiple pages
function ProductPage({ product }: { product: Product }) {
return (
<div>
<title>{product.name} | My Store</title>
<meta name="description" content={product.description} />
{/* Structured data */}
<script type="application/ld+json">
{JSON.stringify({
'@context': 'https://schema.org',
'@type': 'Product',
name: product.name,
description: product.description,
price: product.price,
})}
</script>
<h1>{product.name}</h1>
<p>{product.description}</p>
</div>
);
}
Stylesheet Management
// Stylesheet priority management
function ComponentWithStyles() {
return (
<>
{/* Control load order with precedence */}
<link
rel="stylesheet"
href="/styles/base.css"
precedence="default"
/>
<link
rel="stylesheet"
href="/styles/components.css"
precedence="default"
/>
<link
rel="stylesheet"
href="/styles/utilities.css"
precedence="high"
/>
<div className="styled-component">
Content
</div>
</>
);
}
// Dynamic stylesheets
function ThemeSwitcher({ theme }: { theme: 'light' | 'dark' }) {
return (
<>
<link
rel="stylesheet"
href={`/themes/${theme}.css`}
precedence="high"
/>
<div>Themed content</div>
</>
);
}
Resource Preloading
// Resource preloading API
import { prefetchDNS, preconnect, preload, preinit } from 'react-dom';
function ResourceHints() {
// DNS prefetch
prefetchDNS('https://api.example.com');
// Preconnect
preconnect('https://cdn.example.com');
// Resource preload
preload('/fonts/custom.woff2', {
as: 'font',
type: 'font/woff2',
crossOrigin: 'anonymous',
});
// Script preinit
preinit('/scripts/analytics.js', {
as: 'script',
});
return <div>Content</div>;
}
// Image preloading
function ImageGallery({ images }: { images: string[] }) {
// Preload next images
useEffect(() => {
images.slice(1, 4).forEach((src) => {
preload(src, { as: 'image' });
});
}, [images]);
return (
<div>
{images.map((src) => (
<img key={src} src={src} alt="" />
))}
</div>
);
}
React Compiler (Experimental)
// Automatic memoization with React Compiler
// Before: Manual useMemo/useCallback required
function ProductListOld({ products, onSelect }: Props) {
const sortedProducts = useMemo(
() => [...products].sort((a, b) => a.price - b.price),
[products]
);
const handleSelect = useCallback(
(id: string) => onSelect(id),
[onSelect]
);
return (
<ul>
{sortedProducts.map((product) => (
<ProductItem
key={product.id}
product={product}
onSelect={handleSelect}
/>
))}
</ul>
);
}
// After: React Compiler auto-memoizes
function ProductList({ products, onSelect }: Props) {
// No manual memoization needed - compiler optimizes
const sortedProducts = [...products].sort((a, b) => a.price - b.price);
return (
<ul>
{sortedProducts.map((product) => (
<ProductItem
key={product.id}
product={product}
onSelect={(id) => onSelect(id)}
/>
))}
</ul>
);
}
// Enable React Compiler in babel.config.js
module.exports = {
plugins: [
['babel-plugin-react-compiler', {
// options
}],
],
};
Improved Error Handling
// Improved error display
// Detailed hydration error display
// React 19 shows specific differences
// Improved ErrorBoundary
class ErrorBoundary extends React.Component<
{ children: React.ReactNode; fallback: React.ReactNode },
{ hasError: boolean; error: Error | null }
> {
state = { hasError: false, error: null };
static getDerivedStateFromError(error: Error) {
return { hasError: true, error };
}
componentDidCatch(error: Error, info: React.ErrorInfo) {
// React 19: More detailed stack trace
console.error('Error:', error);
console.error('Component Stack:', info.componentStack);
}
render() {
if (this.state.hasError) {
return this.props.fallback;
}
return this.props.children;
}
}
// Usage
function App() {
return (
<ErrorBoundary fallback={<ErrorPage />}>
<MainContent />
</ErrorBoundary>
);
}
Migration Guide
Migrating from React 18 to 19
# Update packages
npm install react@19 react-dom@19
# TypeScript type definitions
npm install -D @types/react@19 @types/react-dom@19
// Main changes
// 1. forwardRef → normal prop
// Before
const Input = forwardRef<HTMLInputElement, Props>((props, ref) => (
<input ref={ref} {...props} />
));
// After
function Input({ ref, ...props }: Props & { ref?: Ref<HTMLInputElement> }) {
return <input ref={ref} {...props} />;
}
// 2. useContext → use (optional)
// Before
const theme = useContext(ThemeContext);
// After (convenient for conditional use)
const theme = use(ThemeContext);
// 3. Deprecated API removal
// - defaultProps (function components)
// - propTypes
// - createFactory
// - render (react-dom)
// defaultProps alternative
// Before
function Button({ size = 'medium' }) { ... }
Button.defaultProps = { size: 'medium' };
// After (use default parameters)
function Button({ size = 'medium' }: { size?: 'small' | 'medium' | 'large' }) {
// ...
}
Summary
React 19 provides many new features that significantly improve developer experience.
Key New Features
| Feature | Purpose |
|---|---|
| use() | Read Promise/Context |
| useActionState | Form action state management |
| useFormStatus | Get submission status |
| useOptimistic | Optimistic updates |
| Server Actions | Server-side processing |
| ref as prop | No forwardRef needed |
Migration Recommendations
- New projects: Adopt React 19
- Existing projects: Migrate gradually
- Server Actions: Combine with Next.js 14+
React 19 enables more intuitive and faster React application development.