React 19 Features
Leverage cutting-edge React 19 server-side capabilities
React 19 Features
Master React 19's latest capabilities with free flashcards and spaced repetition practice. This lesson covers the new Actions API, improved Server Components, enhanced use hook, and automatic batching improvementsβessential features for building modern, performant React applications.
Welcome to React 19! π»
React 19 represents a significant leap forward in the React ecosystem, introducing features that fundamentally change how we handle asynchronous operations, manage server-client interactions, and optimize rendering performance. Released in 2024, this version focuses on developer experience and performance optimization while maintaining backward compatibility with most existing React applications.
These new features aren't just incremental improvementsβthey represent paradigm shifts in how we think about state management, data fetching, and the separation between client and server code. Whether you're building a simple dashboard or a complex enterprise application, React 19's features will streamline your development workflow.
Core Concepts π―
1. Actions API π
The Actions API is React 19's most transformative feature. It provides a unified way to handle asynchronous operations, particularly form submissions and data mutations, without the boilerplate code traditionally required for loading states, error handling, and optimistic updates.
What are Actions?
Actions are functions that can be passed to form elements or called manually to perform asynchronous operations. React automatically tracks the pending state of these actions and provides hooks to access their status.
function updateName(formData) {
return fetch('/api/user', {
method: 'POST',
body: formData
});
}
function ProfileForm() {
return (
<form action={updateName}>
<input name="username" />
<button type="submit">Update</button>
</form>
);
}
Key Benefits:
- Automatic pending states: No need to manually track
isLoading - Error boundaries integration: Errors are caught and handled gracefully
- Optimistic updates: Update UI before server confirmation
- Progressive enhancement: Forms work even without JavaScript
The useActionState Hook:
This hook provides access to the current state of an action, including pending status and return values.
import { useActionState } from 'react';
function MyForm() {
const [state, submitAction, isPending] = useActionState(
async (previousState, formData) => {
const name = formData.get('name');
return await updateUser(name);
},
null // initial state
);
return (
<form action={submitAction}>
<input name="name" disabled={isPending} />
<button disabled={isPending}>
{isPending ? 'Saving...' : 'Save'}
</button>
{state?.error && <p>{state.error}</p>}
</form>
);
}
π‘ Tip: The useActionState hook replaces the older useFormState hook. If you're migrating from React 18, update your imports!
2. Enhanced use Hook π£
React 19 introduces a revolutionary use hook that can read the value of a Promise or Context. Unlike other hooks, use can be called conditionally and even inside loopsβbreaking the traditional "Rules of Hooks."
Reading Promises with use:
import { use } from 'react';
function UserProfile({ userPromise }) {
const user = use(userPromise);
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}
function App() {
const userPromise = fetchUser(123);
return (
<Suspense fallback={<Loading />}>
<UserProfile userPromise={userPromise} />
</Suspense>
);
}
Reading Context with use:
import { use, createContext } from 'react';
const ThemeContext = createContext('light');
function Button({ primary }) {
// Can be called conditionally!
const theme = primary ? use(ThemeContext) : 'default';
return <button className={theme}>Click me</button>;
}
Why This Matters:
| Traditional Approach | With use Hook |
|---|---|
| Fetch in useEffect, store in state | Pass Promise directly, let React handle it |
| Manual loading/error states | Suspense boundaries handle loading automatically |
| Cannot conditionally read context | Conditional context reading is allowed |
| Waterfall loading issues | Parallel data fetching out of the box |
π€ Did you know? The use hook is the first React hook that can be called conditionally. This breaks a fundamental rule that existed since hooks were introduced in React 16.8!
3. Server Components Improvements π₯οΈ
React 19 significantly enhances Server Components, making them more powerful and easier to use. Server Components render on the server and send only the resulting HTML and minimal JavaScript to the client.
New Server Component Features:
// app/products/page.jsx (Server Component)
import { db } from '@/lib/database';
export default async function ProductsPage() {
// Direct database access - no API route needed!
const products = await db.query('SELECT * FROM products');
return (
<div>
{products.map(product => (
<ProductCard key={product.id} {...product} />
))}
</div>
);
}
Server Actions:
Server Actions are async functions that run on the server but can be called from client components:
'use server'
export async function createProduct(formData) {
const name = formData.get('name');
const price = formData.get('price');
await db.insert({ name, price });
revalidatePath('/products');
}
Client component using the server action:
'use client'
import { createProduct } from './actions';
import { useActionState } from 'react';
export default function ProductForm() {
const [state, formAction] = useActionState(createProduct, null);
return (
<form action={formAction}>
<input name="name" required />
<input name="price" type="number" required />
<button type="submit">Create Product</button>
</form>
);
}
Benefits of Server Components:
- Zero bundle size: Server Components don't add to your JavaScript bundle
- Direct backend access: Query databases, file systems, or internal APIs directly
- Automatic code splitting: Only client component code is shipped to browsers
- SEO friendly: Fully rendered HTML sent to crawlers
π Real-world analogy: Think of Server Components like a restaurant kitchen (server) vs. the dining area (client). The kitchen (server) does all the heavy preparation work, and only the finished dish (rendered HTML) comes to your table (browser).
4. Document Metadata Management π
React 19 introduces native support for document metadata like titles and meta tags, eliminating the need for third-party libraries like react-helmet.
function BlogPost({ post }) {
return (
<>
<title>{post.title} - My Blog</title>
<meta name="description" content={post.excerpt} />
<meta property="og:image" content={post.image} />
<link rel="canonical" href={post.url} />
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
</>
);
}
React automatically hoists these tags to the document <head>, regardless of where they appear in your component tree. This works seamlessly with Server Components and Suspense.
5. Asset Loading Optimization π¨
React 19 provides new APIs for optimizing resource loading, helping you eliminate layout shifts and improve Core Web Vitals scores.
Preloading Resources:
import { preload, preinit } from 'react-dom';
function MyComponent() {
// Preload a resource
preload('/fonts/CustomFont.woff2', { as: 'font' });
// Preinit a stylesheet (loads and applies immediately)
preinit('/styles/critical.css', { as: 'style' });
return <div>Content here</div>;
}
Suspense for Images:
React 19 improves Suspense integration with images:
function Gallery({ images }) {
return (
<Suspense fallback={<Skeleton />}>
{images.map(img => (
<img
key={img.id}
src={img.url}
loading="lazy"
fetchPriority="high"
/>
))}
</Suspense>
);
}
6. useOptimistic Hook π―
The useOptimistic hook allows you to show optimistic UI updates while an asynchronous action is in progress, then revert if the action fails.
import { useOptimistic } from 'react';
function TodoList({ todos, addTodo }) {
const [optimisticTodos, addOptimisticTodo] = useOptimistic(
todos,
(state, newTodo) => [...state, { ...newTodo, pending: true }]
);
async function handleSubmit(formData) {
const newTodo = { id: Date.now(), text: formData.get('text') };
addOptimisticTodo(newTodo);
await addTodo(newTodo);
}
return (
<>
<form action={handleSubmit}>
<input name="text" />
<button>Add</button>
</form>
<ul>
{optimisticTodos.map(todo => (
<li key={todo.id} style={{ opacity: todo.pending ? 0.5 : 1 }}>
{todo.text}
</li>
))}
</ul>
</>
);
}
π‘ Tip: Use useOptimistic for operations like likes, follows, or adding items to a cartβanywhere instant feedback improves user experience.
7. ref as a Prop π―
In React 19, you can now pass ref as a regular prop instead of using forwardRef. This significantly simplifies component APIs.
Before (React 18):
import { forwardRef } from 'react';
const MyInput = forwardRef((props, ref) => {
return <input ref={ref} {...props} />;
});
After (React 19):
function MyInput({ ref, ...props }) {
return <input ref={ref} {...props} />;
}
This works automatically in function components. No forwardRef wrapper needed!
8. Improved Error Handling π‘οΈ
React 19 enhances error reporting with better error boundaries and hydration error messages.
function ErrorBoundary({ fallback, children }) {
return (
<ErrorBoundary
fallback={({ error, resetError }) => (
<div>
<h2>Something went wrong</h2>
<p>{error.message}</p>
<button onClick={resetError}>Try again</button>
</div>
)}
>
{children}
</ErrorBoundary>
);
}
Hydration mismatches now show detailed diffs, making debugging much easier:
Hydration error:
Expected server HTML:
<div>Server: 2024-01-15</div>
Received client HTML:
<div>Client: 2024-01-16</div>
Detailed Examples π¨
Example 1: Building a Form with Actions API
Let's build a complete contact form using React 19's Actions API:
'use client'
import { useActionState } from 'react';
async function submitContactForm(previousState, formData) {
const name = formData.get('name');
const email = formData.get('email');
const message = formData.get('message');
// Validation
if (!name || name.length < 2) {
return { error: 'Name must be at least 2 characters' };
}
if (!email.includes('@')) {
return { error: 'Please enter a valid email' };
}
try {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1000));
const response = await fetch('/api/contact', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, email, message })
});
if (!response.ok) throw new Error('Failed to submit');
return { success: 'Message sent successfully!' };
} catch (error) {
return { error: 'Failed to send message. Please try again.' };
}
}
export default function ContactForm() {
const [state, formAction, isPending] = useActionState(
submitContactForm,
null
);
return (
<form action={formAction} className="contact-form">
<div>
<label htmlFor="name">Name:</label>
<input
id="name"
name="name"
required
disabled={isPending}
/>
</div>
<div>
<label htmlFor="email">Email:</label>
<input
id="email"
name="email"
type="email"
required
disabled={isPending}
/>
</div>
<div>
<label htmlFor="message">Message:</label>
<textarea
id="message"
name="message"
required
disabled={isPending}
/>
</div>
<button type="submit" disabled={isPending}>
{isPending ? 'Sending...' : 'Send Message'}
</button>
{state?.error && (
<div className="error" role="alert">
β {state.error}
</div>
)}
{state?.success && (
<div className="success" role="status">
β
{state.success}
</div>
)}
</form>
);
}
Key Features:
- Automatic pending state management with
isPending - Built-in error handling through return values
- Form inputs automatically disabled during submission
- No manual
useStateoruseEffectneeded! - Progressive enhancement: works without JavaScript
Example 2: Data Fetching with the use Hook
Here's how to build a user dashboard that fetches data in parallel:
import { use, Suspense } from 'react';
// Fetch functions return Promises
function fetchUser(id) {
return fetch(`/api/users/${id}`).then(r => r.json());
}
function fetchPosts(userId) {
return fetch(`/api/posts?user=${userId}`).then(r => r.json());
}
function fetchComments(userId) {
return fetch(`/api/comments?user=${userId}`).then(r => r.json());
}
// Component reads from Promise using 'use'
function UserInfo({ userPromise }) {
const user = use(userPromise);
return (
<div className="user-info">
<img src={user.avatar} alt={user.name} />
<h2>{user.name}</h2>
<p>{user.bio}</p>
</div>
);
}
function PostsList({ postsPromise }) {
const posts = use(postsPromise);
return (
<div className="posts">
<h3>Recent Posts</h3>
{posts.map(post => (
<article key={post.id}>
<h4>{post.title}</h4>
<p>{post.excerpt}</p>
</article>
))}
</div>
);
}
function CommentsList({ commentsPromise }) {
const comments = use(commentsPromise);
return (
<div className="comments">
<h3>Recent Comments</h3>
{comments.map(comment => (
<div key={comment.id}>
<p>{comment.text}</p>
</div>
))}
</div>
);
}
// Main dashboard component
export default function Dashboard({ userId }) {
// Start all fetches immediately (parallel!)
const userPromise = fetchUser(userId);
const postsPromise = fetchPosts(userId);
const commentsPromise = fetchComments(userId);
return (
<div className="dashboard">
<Suspense fallback={<UserSkeleton />}>
<UserInfo userPromise={userPromise} />
</Suspense>
<div className="content-grid">
<Suspense fallback={<PostsSkeleton />}>
<PostsList postsPromise={postsPromise} />
</Suspense>
<Suspense fallback={<CommentsSkeleton />}>
<CommentsList commentsPromise={commentsPromise} />
</Suspense>
</div>
</div>
);
}
Why This Pattern is Powerful:
Traditional useEffect Pattern:
βββββββββββββββββββββββββββββββββββββββ
β Component Mounts β
βββββββββββββ¬ββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββ
β Fetch User (500ms) β
βββββββββ¬ββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββ
β Fetch Posts (300ms) β β Waterfall!
βββββββββ¬ββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββ
β Fetch Comments (200ms)β
βββββββββββββββββββββββββ
Total: 1000ms β±οΈ
React 19 use Hook Pattern:
βββββββββββββββββββββββββββββββββββββββ
β Component Mounts β
βββββββββββββ¬ββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββ
β Fetch User (500ms) β ββ
βββββββββββββββββββββββββ β
βββββββββββββββββββββββββ β Parallel!
β Fetch Posts (300ms) β ββ€
βββββββββββββββββββββββββ β
βββββββββββββββββββββββββ β
β Fetch Comments (200ms)β ββ
βββββββββββββββββββββββββ
Total: 500ms β±οΈ
Example 3: Optimistic UI Updates
Building a like button with instant feedback:
'use client'
import { useOptimistic } from 'react';
async function likePost(postId) {
const response = await fetch(`/api/posts/${postId}/like`, {
method: 'POST'
});
if (!response.ok) throw new Error('Failed to like post');
return response.json();
}
function Post({ id, title, content, initialLikes, initialLiked }) {
const [optimisticLikes, setOptimisticLikes] = useOptimistic(
{ count: initialLikes, liked: initialLiked },
(state, newLiked) => ({
count: state.count + (newLiked ? 1 : -1),
liked: newLiked
})
);
async function handleLike() {
try {
const newLiked = !optimisticLikes.liked;
setOptimisticLikes(newLiked);
await likePost(id);
} catch (error) {
// React automatically reverts on error
console.error('Failed to update like:', error);
}
}
return (
<article>
<h2>{title}</h2>
<p>{content}</p>
<button
onClick={handleLike}
className={optimisticLikes.liked ? 'liked' : ''}
>
{optimisticLikes.liked ? 'β€οΈ' : 'π€'}
{optimisticLikes.count}
</button>
</article>
);
}
The UI updates instantly when you click, then reverts automatically if the server request fails. This creates a snappy, app-like experience.
Example 4: Server Actions with Server Components
Building a blog with server-side rendering and mutations:
// app/blog/actions.js
'use server'
import { revalidatePath } from 'next/cache';
import { db } from '@/lib/database';
export async function createPost(formData) {
const title = formData.get('title');
const content = formData.get('content');
const author = formData.get('author');
if (!title || !content) {
return { error: 'Title and content are required' };
}
try {
await db.posts.insert({
title,
content,
author,
createdAt: new Date()
});
revalidatePath('/blog');
return { success: true };
} catch (error) {
return { error: 'Failed to create post' };
}
}
export async function deletePost(postId) {
await db.posts.delete({ id: postId });
revalidatePath('/blog');
}
// app/blog/page.jsx (Server Component)
import { db } from '@/lib/database';
import { createPost } from './actions';
import PostForm from './PostForm';
export default async function BlogPage() {
// Direct database access on server!
const posts = await db.posts.findMany({
orderBy: { createdAt: 'desc' }
});
return (
<div>
<h1>Blog</h1>
<PostForm createAction={createPost} />
<div className="posts">
{posts.map(post => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.content}</p>
<small>By {post.author}</small>
</article>
))}
</div>
</div>
);
}
// app/blog/PostForm.jsx (Client Component)
'use client'
import { useActionState } from 'react';
export default function PostForm({ createAction }) {
const [state, formAction, isPending] = useActionState(
createAction,
null
);
return (
<form action={formAction}>
<input
name="title"
placeholder="Post title"
disabled={isPending}
/>
<textarea
name="content"
placeholder="Post content"
disabled={isPending}
/>
<input
name="author"
placeholder="Your name"
disabled={isPending}
/>
<button type="submit" disabled={isPending}>
{isPending ? 'Publishing...' : 'Publish Post'}
</button>
{state?.error && <div className="error">{state.error}</div>}
{state?.success && <div className="success">Post published!</div>}
</form>
);
}
Architecture Overview:
ββββββββββββββββββββββββββββββββββββββββββββββββββ
β CLIENT BROWSER β
β β
β ββββββββββββββββββββββββββββββββββββ β
β β PostForm (Client Component) β β
β β β’ Handles user interaction β β
β β β’ Shows loading states β β
β β β’ Calls server action β β
β βββββββββββββββ¬βββββββββββββββββββββ β
β β β
ββββββββββββββββββΌββββββββββββββββββββββββββββββββ
β formAction()
βΌ
ββββββββββββββββββββββββββββββββββββββββββββββββββ
β SERVER β
β β
β ββββββββββββββββββββββββββββββββββββ β
β β createPost (Server Action) β β
β β β’ Validates input β β
β β β’ Writes to database β β
β β β’ Triggers revalidation β β
β βββββββββββββββ¬βββββββββββββββββββββ β
β β β
β βΌ β
β ββββββββββββββββββββββββββββββββββββ β
β β BlogPage (Server Component) β β
β β β’ Fetches latest posts β β
β β β’ Renders HTML on server β β
β ββββββββββββββββββββββββββββββββββββ β
β β
ββββββββββββββββββββββββββββββββββββββββββββββββββ
Common Mistakes β οΈ
1. β Using use() in Regular Hooks
// WRONG - use() is not a replacement for useEffect
function MyComponent() {
const data = use(fetch('/api/data')); // Missing Suspense boundary!
return <div>{data.name}</div>;
}
// CORRECT - Wrap with Suspense
function App() {
return (
<Suspense fallback={<Loading />}>
<MyComponent />
</Suspense>
);
}
2. β Forgetting to Mark Server Functions
// WRONG - Missing 'use server' directive
export async function updateUser(data) {
await db.users.update(data);
}
// CORRECT
'use server'
export async function updateUser(data) {
await db.users.update(data);
}
3. β Mixing Client and Server Code Improperly
// WRONG - Trying to use browser APIs in Server Component
export default async function Page() {
const width = window.innerWidth; // Error! No window on server
return <div>Width: {width}</div>;
}
// CORRECT - Mark as client component
'use client'
export default function Page() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
const handleResize = () => setWidth(window.innerWidth);
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return <div>Width: {width}</div>;
}
4. β Not Handling Action Errors Properly
// WRONG - Unhandled errors crash the app
function MyForm() {
const [state, formAction] = useActionState(async (prev, formData) => {
await fetch('/api/submit', { body: formData }); // Might throw!
return { success: true };
}, null);
return <form action={formAction}>...</form>;
}
// CORRECT - Always catch and return errors
function MyForm() {
const [state, formAction] = useActionState(async (prev, formData) => {
try {
const response = await fetch('/api/submit', {
method: 'POST',
body: formData
});
if (!response.ok) {
throw new Error('Submission failed');
}
return { success: true };
} catch (error) {
return { error: error.message };
}
}, null);
return (
<form action={formAction}>
{/* form fields */}
{state?.error && <div className="error">{state.error}</div>}
</form>
);
}
5. β Creating New Promises on Every Render
// WRONG - New promise on every render!
function UserProfile({ userId }) {
const user = use(fetchUser(userId)); // fetchUser() called each render
return <div>{user.name}</div>;
}
// CORRECT - Pass promise from parent
function App({ userId }) {
const userPromise = useMemo(
() => fetchUser(userId),
[userId]
);
return (
<Suspense fallback={<Loading />}>
<UserProfile userPromise={userPromise} />
</Suspense>
);
}
function UserProfile({ userPromise }) {
const user = use(userPromise);
return <div>{user.name}</div>;
}
6. β Misusing forwardRef When It's No Longer Needed
// WRONG in React 19 - forwardRef is unnecessary
import { forwardRef } from 'react';
const MyInput = forwardRef((props, ref) => {
return <input ref={ref} {...props} />;
});
// CORRECT - ref is now a regular prop
function MyInput({ ref, ...props }) {
return <input ref={ref} {...props} />;
}
π§ Memory Device for React 19 Features:
"AURA SO"
- Actions API
- Use hook
- Ref as prop
- Asset preloading
- Server Components
- Optimistic updates
Key Takeaways π―
β Actions API simplifies async operations with automatic pending states and error handling
β The use hook can read Promises and Context, even conditionallyβbreaking traditional hook rules
β Server Components let you access backend resources directly without API routes
β Server Actions enable mutations from client components while keeping business logic on the server
β useOptimistic provides instant UI feedback with automatic rollback on errors
β
ref as a prop eliminates the need for forwardRef in most cases
β Native metadata management removes dependency on third-party libraries
β Improved asset preloading APIs optimize performance and Core Web Vitals
β Enhanced error boundaries provide better debugging information
β React 19 maintains backward compatibility with most React 18 code
π Further Study
π Quick Reference Card
| Feature | Hook/API | Key Use Case |
|---|---|---|
| Actions | useActionState() |
Form submissions with auto pending states |
| Promise Reading | use(promise) |
Async data fetching without useEffect |
| Optimistic UI | useOptimistic() |
Instant feedback for mutations |
| Server Actions | 'use server' |
Backend mutations from client components |
| Asset Loading | preload(), preinit() |
Optimize resource loading |
| Ref Passing | ref as prop |
No forwardRef needed |
| Metadata | <title>, <meta> |
Native document head management |
π§ Try This: Migration Checklist
Moving from React 18 to React 19:
β Replace forwardRef with ref as prop
β Update useFormState to useActionState
β Remove react-helmet in favor of native metadata
β Consider Server Components for data-heavy pages
β Use Actions API for form submissions
β Add use() hook for cleaner async data fetching
β Implement useOptimistic() for better UX
β Test with Suspense boundaries around async components