Skip to main content

React 19 Features and Migration Guide: What's New and How to Upgrade

Master React 19's new features including Actions API, Server Components, new hooks, and the React Compiler. Learn migration strategies and best practices for upgrading from React 18.

Table of Contents

Introduction

React 19 represents one of the most significant updates to the React library since React 18 introduced concurrent features. Released in late 2024, React 19 brings powerful new capabilities that simplify form handling, improve performance through automatic optimizations, and enhance the developer experience with better tooling and error messages.

The release introduces several groundbreaking features: the Actions API for streamlined form submissions, new hooks like useActionState and useOptimistic for better async state management, enhanced Server Components support, and the React Compiler that automatically optimizes your code without manual memoization.

However, React 19 also includes breaking changes that require careful migration. Deprecated APIs like ReactDOM.render have been removed, string refs are no longer supported, and some patterns that worked in React 18 need updates. Understanding these changes is crucial for a smooth upgrade path.

This comprehensive guide will walk you through React 19’s new features, show you practical examples of how to use them, and provide a step-by-step migration strategy. Whether you’re building a new application or upgrading an existing React 18 codebase, you’ll learn how to leverage React 19’s capabilities effectively.

By the end of this guide, you’ll understand React 19’s major features, know how to migrate your codebase, and be equipped with best practices for building modern React applications.


What’s New in React 19

React 19 introduces a comprehensive set of features designed to improve performance, simplify common patterns, and enhance the developer experience. Let’s explore the major additions:

Key Features Overview

FeatureDescriptionImpact
Actions APISimplified form handling with automatic state managementReduces boilerplate, improves UX
useActionStateHook for managing async action statesBetter form state handling
useOptimisticOptimistic UI updates for better perceived performanceImmediate user feedback
useFormStatusAccess form submission status from child componentsBetter form UX
Server ComponentsEnhanced support for server-side renderingSmaller bundles, faster loads
React CompilerAutomatic memoization and optimizationLess manual optimization needed
Enhanced Concurrent RenderingImproved scheduling and batchingBetter performance

Prerequisites

Before diving into React 19 features, ensure you have:

  • Node.js 18+: Required for React 19
  • React 19.0.0+: Latest stable version
  • React DOM 19.0.0+: Matching version required
  • TypeScript 5.0+ (optional but recommended): For better type safety

💡 Tip: React 19 maintains backward compatibility with React 18 code, so you can upgrade incrementally. Most new features are opt-in, meaning your existing code will continue to work.


Actions API: Simplifying Form Handling

The Actions API is one of React 19’s most impactful features, dramatically simplifying form handling and server mutations. It automatically manages pending states, errors, and form submissions, reducing boilerplate code significantly.

Understanding Actions

An action is an async function that can be passed directly to form elements. React automatically handles the form submission lifecycle:

// Server Action (works in Server Components)
async function createUser(formData: FormData) {
'use server'; // Required for Server Actions
const name = formData.get('name') as string;
const email = formData.get('email') as string;
// Validate and save to database
await saveUser({ name, email });
return { success: true, message: 'User created successfully' };
}
// Usage in Server Component
export default function UserForm() {
return (
<form action={createUser}>
<input name="name" type="text" required />
<input name="email" type="email" required />
<button type="submit">Create User</button>
</form>
);
}

Client Actions with useActionState

For client-side actions, use the useActionState hook to manage form state:

'use client';
import { useActionState } from 'react';
// Action function receives previous state and form data
async function addTodo(prevState: any, formData: FormData) {
const todo = formData.get('todo') as string;
if (!todo || todo.trim().length === 0) {
return { error: 'Todo cannot be empty' };
}
try {
await fetch('/api/todos', {
method: 'POST',
body: JSON.stringify({ text: todo }),
});
return { success: true, message: 'Todo added successfully' };
} catch (error) {
return { error: 'Failed to add todo' };
}
}
function TodoForm() {
// useActionState manages pending state, errors, and form data automatically
const [state, formAction, isPending] = useActionState(addTodo, null);
return (
<form action={formAction}>
<input
name="todo"
type="text"
disabled={isPending}
placeholder="Add a todo..."
/>
<button type="submit" disabled={isPending}>
{isPending ? 'Adding...' : 'Add Todo'}
</button>
{state?.error && <p className="error">{state.error}</p>}
{state?.success && <p className="success">{state.message}</p>}
</form>
);
}

Best Practice: Use useActionState for client-side form handling. It automatically manages pending states, preventing duplicate submissions and providing better UX.

Server Actions in Next.js

Server Actions work seamlessly with Next.js App Router (Next.js 13+):

app/actions/todos.ts
'use server';
import { revalidatePath } from 'next/cache';
export async function createTodo(formData: FormData) {
const text = formData.get('text') as string;
// Direct database access (no API route needed!)
await db.todos.create({
data: { text, completed: false },
});
// Revalidate the todos page
revalidatePath('/todos');
return { success: true };
}
// app/todos/page.tsx
import { createTodo } from './actions/todos';
export default function TodosPage() {
return (
<form action={createTodo}>
<input name="text" type="text" required />
<button type="submit">Add Todo</button>
</form>
);
}

⚠️ Important: Server Actions require the 'use server' directive and can only be used in Server Components or passed to Client Components as props. They enable direct database access without API routes, reducing complexity.


New Hooks for Asynchronous UI

React 19 introduces three powerful hooks that simplify asynchronous UI patterns: useActionState, useOptimistic, and useFormStatus. These hooks work together to create responsive, user-friendly interfaces.

useActionState Hook

useActionState is the foundation for managing async actions in React 19. It combines state management with action handling:

'use client';
import { useActionState } from 'react';
type ActionState = {
error?: string;
success?: boolean;
data?: any;
};
async function updateProfile(
prevState: ActionState | null,
formData: FormData
): Promise<ActionState> {
const name = formData.get('name') as string;
if (name.length < 3) {
return { error: 'Name must be at least 3 characters' };
}
try {
const response = await fetch('/api/profile', {
method: 'PUT',
body: JSON.stringify({ name }),
});
if (!response.ok) {
throw new Error('Update failed');
}
return { success: true, data: await response.json() };
} catch (error) {
return { error: 'Failed to update profile' };
}
}
function ProfileForm() {
const [state, formAction, isPending] = useActionState(updateProfile, null);
return (
<form action={formAction}>
<input
name="name"
type="text"
disabled={isPending}
defaultValue={state?.data?.name}
/>
<button type="submit" disabled={isPending}>
{isPending ? 'Saving...' : 'Save Profile'}
</button>
{state?.error && (
<div className="error-message">{state.error}</div>
)}
{state?.success && (
<div className="success-message">Profile updated!</div>
)}
</form>
);
}

useOptimistic Hook

useOptimistic enables optimistic UI updates, showing immediate feedback while the server processes the request:

'use client';
import { useOptimistic, useActionState } from 'react';
async function addComment(
prevState: any,
formData: FormData
) {
const text = formData.get('text') as string;
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1000));
return {
comments: [...(prevState?.comments || []), { id: Date.now(), text }],
};
}
function CommentsList({ comments }: { comments: Array<{ id: number; text: string }> }) {
const [state, formAction] = useActionState(addComment, { comments });
// Optimistic update: show comment immediately, revert if fails
const [optimisticComments, addOptimisticComment] = useOptimistic(
state.comments,
(currentComments, newComment: { id: number; text: string }) => [
...currentComments,
newComment,
]
);
async function handleSubmit(formData: FormData) {
const text = formData.get('text') as string;
// Optimistically add comment
addOptimisticComment({ id: Date.now(), text });
// Submit to server
await formAction(formData);
}
return (
<div>
<ul>
{optimisticComments.map(comment => (
<li key={comment.id}>{comment.text}</li>
))}
</ul>
<form action={handleSubmit}>
<input name="text" type="text" required />
<button type="submit">Add Comment</button>
</form>
</div>
);
}

Best Practice: Use useOptimistic for actions where immediate feedback improves UX, like likes, comments, or toggles. Users see instant updates, making the app feel faster.

useFormStatus Hook

useFormStatus provides form submission status from within child components, enabling better form UX:

'use client';
import { useFormStatus } from 'react';
function SubmitButton() {
const { pending, data, method, action } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? 'Submitting...' : 'Submit'}
</button>
);
}
function FormStatus() {
const { pending } = useFormStatus();
if (pending) {
return <div className="spinner">Processing...</div>;
}
return null;
}
function ContactForm() {
return (
<form action={handleSubmit}>
<input name="email" type="email" required />
<input name="message" type="text" required />
<FormStatus />
<SubmitButton />
</form>
);
}

💡 Tip: useFormStatus must be used within a component that’s a descendant of a <form> element. It automatically tracks the nearest form’s submission status.


Server Components and Server Actions

React 19 enhances Server Components support, making them more powerful and easier to use. Server Components render on the server, reducing client-side JavaScript and improving performance.

Understanding Server Components

Server Components are React components that render exclusively on the server. They can’t use client-side features like hooks, event handlers, or browser APIs:

// ✅ Server Component (default in Next.js App Router)
async function BlogPost({ slug }: { slug: string }) {
// Direct database access - no API route needed!
const post = await db.posts.findUnique({
where: { slug },
});
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
);
}
// ❌ Can't use hooks in Server Components
async function InvalidServerComponent() {
const [state, setState] = useState(0); // Error!
return <div>{state}</div>;
}

Server Actions Integration

Server Actions work seamlessly with Server Components, enabling server-side mutations:

app/actions/posts.ts
'use server';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
export async function createPost(formData: FormData) {
const title = formData.get('title') as string;
const content = formData.get('content') as string;
// Validate
if (!title || !content) {
return { error: 'Title and content are required' };
}
// Save to database
const post = await db.posts.create({
data: { title, content, slug: generateSlug(title) },
});
// Revalidate and redirect
revalidatePath('/posts');
redirect(`/posts/${post.slug}`);
}
// app/posts/new/page.tsx (Server Component)
import { createPost } from '../actions/posts';
export default function NewPostPage() {
return (
<form action={createPost}>
<input name="title" type="text" required />
<textarea name="content" required />
<button type="submit">Create Post</button>
</form>
);
}

Combining Server and Client Components

Server Components can render Client Components, enabling hybrid architectures:

// app/components/PostList.tsx (Server Component)
import { db } from '@/lib/db';
import { PostCard } from './PostCard'; // Client Component
export async function PostList() {
const posts = await db.posts.findMany();
return (
<div>
{posts.map(post => (
<PostCard key={post.id} post={post} />
))}
</div>
);
}
// app/components/PostCard.tsx (Client Component)
'use client';
import { useState } from 'react';
export function PostCard({ post }: { post: Post }) {
const [liked, setLiked] = useState(false);
return (
<div>
<h2>{post.title}</h2>
<button onClick={() => setLiked(!liked)}>
{liked ? '❤️' : '🤍'}
</button>
</div>
);
}

⚠️ Important: Server Components can’t import Client Components directly. You must pass Client Components as children or props. The 'use client' directive creates a boundary between server and client code.


React Compiler: Automatic Optimization

The React Compiler is one of React 19’s most exciting features. It automatically optimizes your code, reducing the need for manual memoization with useMemo, useCallback, and React.memo.

How the React Compiler Works

The React Compiler analyzes your code at build time and automatically adds optimizations:

// Before React Compiler: Manual optimization required
function ExpensiveComponent({ items }: { items: Item[] }) {
const sortedItems = useMemo(
() => items.sort((a, b) => a.name.localeCompare(b.name)),
[items]
);
const handleClick = useCallback(() => {
console.log('Clicked');
}, []);
return (
<div>
{sortedItems.map(item => (
<ItemComponent key={item.id} item={item} onClick={handleClick} />
))}
</div>
);
}
// After React Compiler: Automatic optimization
function ExpensiveComponent({ items }: { items: Item[] }) {
// Compiler automatically memoizes these
const sortedItems = items.sort((a, b) => a.name.localeCompare(b.name));
const handleClick = () => {
console.log('Clicked');
};
return (
<div>
{sortedItems.map(item => (
<ItemComponent key={item.id} item={item} onClick={handleClick} />
))}
</div>
);
}

Enabling the React Compiler

In Next.js 15+, the React Compiler is enabled by default. For other setups, configure it in your build tool:

// next.config.js (Next.js 15+)
module.exports = {
// React Compiler enabled by default
};
// vite.config.js (Vite)
import { reactCompiler } from "vite-plugin-react-compiler";
export default {
plugins: [
react(),
reactCompiler(), // Add React Compiler plugin
],
};

What Gets Optimized

The React Compiler automatically optimizes:

  • Expensive computations: Automatically wrapped in useMemo
  • Event handlers: Automatically wrapped in useCallback
  • Component memoization: Automatically applies React.memo where beneficial
  • Dependency tracking: Automatically tracks dependencies for effects and callbacks

💡 Tip: You can still use useMemo and useCallback if you need explicit control, but the compiler handles most cases automatically. This reduces boilerplate and makes code more readable.


Enhanced Concurrent Rendering

React 19 improves concurrent rendering with better scheduling algorithms and automatic batching, resulting in smoother user experiences and better performance.

Automatic Batching Improvements

React 19 automatically batches more state updates, reducing unnecessary re-renders:

// React 18: Some updates weren't batched
function Component() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
function handleClick() {
setCount(c => c + 1); // Re-render
setFlag(f => !f); // Re-render (not batched in some cases)
}
return <button onClick={handleClick}>Click</button>;
}
// React 19: All updates are automatically batched
function Component() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
function handleClick() {
setCount(c => c + 1); // Batched
setFlag(f => !f); // Batched - single re-render
}
return <button onClick={handleClick}>Click</button>;
}

Improved Scheduling

React 19’s scheduler adapts to user interactions, prioritizing responsive updates:

function SearchResults({ query }: { query: string }) {
const [results, setResults] = useState([]);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
setIsLoading(true);
// React 19 prioritizes user interactions over this update
fetchResults(query).then(data => {
setResults(data);
setIsLoading(false);
});
}, [query]);
return (
<div>
{isLoading && <Spinner />}
{results.map(result => (
<ResultItem key={result.id} result={result} />
))}
</div>
);
}

Best Practice: React 19’s improved scheduling means you can write code more naturally without worrying about performance. The framework handles optimization automatically.


Improved Developer Experience

React 19 includes significant improvements to developer experience, including better error messages, enhanced React DevTools, and improved TypeScript support.

Better Error Messages

React 19 provides more actionable error messages:

// React 18: Generic error
// Error: Cannot read property 'map' of undefined
// React 19: Detailed error with suggestions
// Error: Cannot read property 'map' of undefined
//
// Component: UserList
// Location: src/components/UserList.tsx:15
//
// Suggestion: Add a default value or check if users exists:
// const users = props.users || [];

Enhanced React DevTools

React DevTools in React 19 includes:

  • Better Server Component visualization: See which components render on server vs client
  • Action tracking: Monitor Server Actions and their states
  • Performance insights: Identify optimization opportunities
  • Component tree improvements: Better representation of component hierarchy

TypeScript Improvements

React 19 improves TypeScript support with better type inference:

// Better type inference for refs
function Component() {
const inputRef = useRef<HTMLInputElement>(null);
// TypeScript knows inputRef.current is HTMLInputElement | null
const handleClick = () => {
inputRef.current?.focus(); // Type-safe!
};
return <input ref={inputRef} />;
}
// Improved Server Action types
type ServerAction<T> = (formData: FormData) => Promise<T>;
async function updateUser(formData: FormData): Promise<{ success: boolean }> {
// Implementation
}
// Type-safe action usage
const [state, action] = useActionState(updateUser, null);
// state is inferred as { success: boolean } | null

Breaking Changes and Deprecations

React 19 includes several breaking changes that require updates to your codebase. Understanding these changes is crucial for a smooth migration.

Removed APIs

The following APIs have been removed in React 19:

ReactDOM.render

// ❌ React 18 (deprecated, removed in React 19)
import { render } from 'react-dom';
render(<App />, document.getElementById('root'));
// ✅ React 19 (use createRoot)
import { createRoot } from 'react-dom/client';
const root = createRoot(document.getElementById('root')!);
root.render(<App />);

ReactDOM.hydrate

// ❌ React 18 (deprecated, removed in React 19)
import { hydrate } from 'react-dom';
hydrate(<App />, document.getElementById('root'));
// ✅ React 19 (use hydrateRoot)
import { hydrateRoot } from 'react-dom/client';
hydrateRoot(document.getElementById('root')!, <App />);

String Refs

// ❌ React 18 (deprecated, removed in React 19)
class Component extends React.Component {
render() {
return <input ref="input" />;
}
componentDidMount() {
this.refs.input.focus(); // Error in React 19
}
}
// ✅ React 19 (use callback refs or useRef)
class Component extends React.Component {
inputRef = React.createRef<HTMLInputElement>();
render() {
return <input ref={this.inputRef} />;
}
componentDidMount() {
this.inputRef.current?.focus();
}
}
// Or with hooks:
function Component() {
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
inputRef.current?.focus();
}, []);
return <input ref={inputRef} />;
}

Legacy Context API Removal

The legacy Context API has been fully removed:

// ❌ React 18 (legacy API, removed in React 19)
class Component extends React.Component {
static contextTypes = {
theme: PropTypes.string,
};
render() {
const { theme } = this.context;
return <div className={theme}>Content</div>;
}
}
// ✅ React 19 (use modern Context API)
const ThemeContext = createContext('light');
function Component() {
const theme = useContext(ThemeContext);
return <div className={theme}>Content</div>;
}

createFactory Removal

React.createFactory has been removed:

// ❌ React 18 (removed in React 19)
const createDiv = React.createFactory('div');
const element = createDiv({ className: 'container' }, 'Content');
// ✅ React 19 (use JSX directly)
const element = <div className="container">Content</div>;

Migration Guide: React 18 to React 19

Migrating to React 19 requires careful planning and testing. Follow this step-by-step guide for a smooth upgrade.

Step 1: Audit Dependencies

Before upgrading, ensure all dependencies support React 19:

Terminal window
# Check for React 19 compatibility
pnpm outdated
# Update dependencies
pnpm update
# Check for breaking changes in dependencies
pnpm audit

💡 Tip: Most popular libraries already support React 19. Check their documentation or GitHub issues for compatibility information.

Step 2: Update React and React DOM

Upgrade React and React DOM to version 19:

Terminal window
# Using pnpm (recommended)
pnpm add react@^19.0.0 react-dom@^19.0.0
# Or using npm
npm install react@^19.0.0 react-dom@^19.0.0
# Or using yarn
yarn add react@^19.0.0 react-dom@^19.0.0

Step 3: Run Codemods

React provides codemods to automate migration:

Terminal window
# Run React 19 codemod
npx @react-codemod/react-19
# Or use the interactive codemod
npx @react-codemod/react-19 --interactive

The codemod automatically updates:

  • ReactDOM.rendercreateRoot
  • ReactDOM.hydratehydrateRoot
  • String refs → callback refs or useRef
  • Legacy Context API → modern Context API

Step 4: Update Entry Points

Update your application entry points:

// src/index.tsx (or main.tsx)
import { createRoot } from 'react-dom/client';
import App from './App';
const container = document.getElementById('root');
if (!container) {
throw new Error('Root element not found');
}
const root = createRoot(container);
root.render(<App />);

Step 5: Update Server-Side Rendering

If using SSR, update hydration:

// server.tsx or similar
import { renderToString } from 'react-dom/server';
import App from './App';
export function render() {
const html = renderToString(<App />);
return html;
}
// client.tsx
import { hydrateRoot } from 'react-dom/client';
import App from './App';
const container = document.getElementById('root');
if (container) {
hydrateRoot(container, <App />);
}

Step 6: Replace Deprecated Patterns

Manually update any remaining deprecated patterns:

// Replace string refs
// Before
<input ref="input" />
// After
const inputRef = useRef<HTMLInputElement>(null);
<input ref={inputRef} />
// Replace legacy Context
// Before
static contextTypes = { theme: PropTypes.string };
// After
const theme = useContext(ThemeContext);

Step 7: Test Thoroughly

After migration, test your application:

Terminal window
# Run tests
pnpm test
# Test in development
pnpm dev
# Build and test production build
pnpm build
pnpm preview

⚠️ Important: Pay special attention to:

  • Form submissions and actions
  • Server-side rendering behavior
  • Component refs and DOM access
  • Context usage throughout the app
  • Third-party library compatibility

Step 8: Enable React Compiler (Optional)

If using Next.js 15+, the React Compiler is enabled by default. For other setups:

vite.config.js
import { reactCompiler } from "vite-plugin-react-compiler";
export default {
plugins: [react(), reactCompiler()],
};

Step 9: Update TypeScript Types

Update TypeScript types if needed:

Terminal window
pnpm add -D @types/react@^19.0.0 @types/react-dom@^19.0.0

Best Practices for React 19

Following React 19 best practices ensures optimal performance, maintainability, and developer experience.

Use Actions for Form Handling

Do: Use Actions API for form submissions

async function handleSubmit(formData: FormData) {
'use server';
// Handle submission
}
<form action={handleSubmit}>
{/* form fields */}
</form>

Don’t: Use manual state management for forms

// Avoid manual form state when Actions API works
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState(null);
async function handleSubmit(e: FormEvent) {
e.preventDefault();
setIsSubmitting(true);
// Manual state management...
}

Leverage useOptimistic for Better UX

Do: Use optimistic updates for immediate feedback

const [optimisticTodos, addOptimisticTodo] = useOptimistic(
todos,
(state, newTodo) => [...state, newTodo],
);

Don’t: Wait for server response before updating UI

// Avoid waiting for server response
async function addTodo() {
setIsLoading(true);
const result = await api.addTodo(todo);
setTodos([...todos, result]); // User waits unnecessarily
setIsLoading(false);
}

Prefer Server Components When Possible

Do: Use Server Components for data fetching

// Server Component - no client JavaScript needed
async function BlogPost({ slug }: { slug: string }) {
const post = await db.posts.findUnique({ where: { slug } });
return <article>{post.content}</article>;
}

Don’t: Fetch data in Client Components unnecessarily

// Avoid client-side data fetching when Server Components work
'use client';
function BlogPost({ slug }: { slug: string }) {
const [post, setPost] = useState(null);
useEffect(() => {
fetch(`/api/posts/${slug}`).then(r => r.json()).then(setPost);
}, [slug]);
return post ? <article>{post.content}</article> : <Loading />;
}

Trust the React Compiler

Do: Write code naturally, let the compiler optimize

// Let the compiler handle optimization
function Component({ items }: { items: Item[] }) {
const sorted = items.sort((a, b) => a.name.localeCompare(b.name));
return <div>{sorted.map(item => <Item key={item.id} item={item} />)}</div>;
}

Don’t: Over-optimize manually (unless needed)

// Avoid unnecessary manual optimization
const sorted = useMemo(() => items.sort(...), [items]); // Compiler handles this
const handleClick = useCallback(() => {...}, []); // Compiler handles this

Use TypeScript for Type Safety

Do: Leverage TypeScript’s improved React 19 types

type ActionState = {
error?: string;
success?: boolean;
};
async function action(
prevState: ActionState | null,
formData: FormData,
): Promise<ActionState> {
// Type-safe action
}

Common Pitfalls to Avoid

Avoid these common mistakes when using React 19:

Mixing Server and Client Code

Don’t: Use client-side APIs in Server Components

// ❌ Error: Can't use browser APIs in Server Components
async function ServerComponent() {
const [state, setState] = useState(0); // Error!
useEffect(() => {}, []); // Error!
localStorage.getItem('key'); // Error!
return <div>Content</div>;
}

Do: Separate Server and Client Components

// ✅ Server Component
async function ServerComponent() {
const data = await fetchData();
return <ClientComponent data={data} />;
}
// ✅ Client Component
'use client';
function ClientComponent({ data }: { data: Data }) {
const [state, setState] = useState(0);
return <div>{data.content}</div>;
}

Incorrect useFormStatus Usage

Don’t: Use useFormStatus outside a form

// ❌ Error: useFormStatus must be inside a form
function Component() {
const { pending } = useFormStatus(); // Error!
return <div>Content</div>;
}

Do: Use useFormStatus within form descendants

// ✅ Correct: useFormStatus inside form
function SubmitButton() {
const { pending } = useFormStatus();
return <button disabled={pending}>Submit</button>;
}
function Form() {
return (
<form>
<SubmitButton /> {/* ✅ Works: descendant of form */}
</form>
);
}

Forgetting ‘use server’ Directive

Don’t: Forget the 'use server' directive for Server Actions

// ❌ Error: Missing 'use server' directive
async function createPost(formData: FormData) {
await db.posts.create({ data: formData });
}

Do: Always include 'use server' for Server Actions

// ✅ Correct: Includes 'use server'
"use server";
async function createPost(formData: FormData) {
await db.posts.create({ data: formData });
}

Not Handling Action Errors

Don’t: Ignore action errors

// ❌ No error handling
async function action(prevState: any, formData: FormData) {
await api.createItem(formData);
return { success: true };
}

Do: Always handle errors in actions

// ✅ Proper error handling
async function action(prevState: any, formData: FormData) {
try {
await api.createItem(formData);
return { success: true };
} catch (error) {
return { error: "Failed to create item" };
}
}

Conclusion

React 19 represents a significant leap forward for React development, introducing powerful features that simplify common patterns, improve performance, and enhance developer experience. The Actions API streamlines form handling, new hooks like useActionState and useOptimistic enable better async UI patterns, and the React Compiler automatically optimizes your code.

Server Components and Server Actions enable full-stack React applications with reduced complexity, while enhanced concurrent rendering and automatic batching improve performance without manual optimization. The improved developer experience, with better error messages and enhanced tooling, makes React 19 a joy to work with.

While React 19 includes breaking changes, the migration path is well-documented and most changes can be automated with codemods. The benefits—reduced boilerplate, better performance, and improved developer experience—make the upgrade worthwhile for most applications.

As you adopt React 19, remember to:

  • Use Actions API for form handling to reduce boilerplate
  • Leverage Server Components when possible to reduce bundle size
  • Trust the React Compiler for automatic optimizations
  • Follow migration guides carefully to avoid breaking changes
  • Test thoroughly after upgrading to ensure everything works

React 19 sets the foundation for the future of React development. By understanding its features and best practices, you’ll be well-equipped to build modern, performant React applications.

For more information, check out the official React 19 documentation and explore related topics like React Server Components, state management strategies, and React best practices.