React 19 New Features
React 19 introduces groundbreaking features like Server Components, Actions API, the use() hook, and React Compiler. These features improve both developer experience and performance simultaneously.
Server Components — Concept and How They Work
React Server Components (RSC) are components that render only on the server. They greatly improve initial load performance by not sending JavaScript bundles to the client.
// server-component.jsx (Server Component - default)
// Without 'use client', it's a server component
import { db } from './database';
// Can directly access the database from server components!
async function UserList() {
// Use await directly (server components can be async)
const users = await db.query('SELECT * FROM users');
return (
<ul>
{users.map(user => (
<li key={user.id}>
{user.name} - {user.email}
</li>
))}
</ul>
);
}
export default UserList;
// client-component.jsx (Client Component)
'use client'; // This directive marks it as a client component
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(c => c + 1)}>
Click count: {count}
</button>
);
}
Server vs Client Component Comparison
Server Components:
- Runs on server, not included in bundle
- Can use async/await directly
- Can access DB, filesystem
- Cannot use useState, useEffect
- Cannot use event handlers
- Can safely use API keys and other server secrets
Client Components:
- Runs in browser
- Can use React hooks
- Can manipulate DOM, handle events
- Implements interactive UI
// Including client components inside server components (allowed)
// server-page.jsx
import { ClientComponent } from './client-component';
import { getServerData } from './data';
async function Page({ params }) {
const data = await getServerData(params.id);
return (
<div>
<h1>{data.title}</h1>
{/* Pass data as props from server */}
<ClientComponent initialData={data} />
</div>
);
}
// Cannot include server components inside client components
// (Passing server components as children is allowed)
Actions API
React 19's Actions simplify form submission and async state updates.
useActionState
'use client';
import { useActionState } from 'react';
// Server action (server-actions.js)
'use server';
async function createUser(prevState, formData) {
const name = formData.get('name');
const email = formData.get('email');
try {
const user = await db.users.create({ name, email });
return { success: true, user, error: null };
} catch (err) {
if (err.code === 'DUPLICATE_EMAIL') {
return { success: false, user: null, error: 'Email is already in use' };
}
return { success: false, user: null, error: 'An error occurred' };
}
}
// Client component
function SignupForm() {
const [state, action, isPending] = useActionState(
createUser,
{ success: false, user: null, error: null } // Initial state
);
return (
<form action={action}>
<input name="name" placeholder="Name" required />
<input name="email" type="email" placeholder="Email" required />
{state.error && (
<p className="error">{state.error}</p>
)}
{state.success && (
<p className="success">Welcome, {state.user.name}!</p>
)}
<button type="submit" disabled={isPending}>
{isPending ? 'Processing...' : 'Sign Up'}
</button>
</form>
);
}
useFormStatus
'use client';
import { useFormStatus } from 'react-dom';
// Access parent form state from a child component of the form
function SubmitButton() {
const { pending, data, method, action } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? 'Submitting...' : 'Submit'}
</button>
);
}
function Form() {
return (
<form action={serverAction}>
<input name="title" placeholder="Title" />
<SubmitButton /> {/* Automatically accesses parent form state */}
</form>
);
}
useOptimistic
'use client';
import { useOptimistic, useActionState } from 'react';
function TodoList({ todos, addTodo }) {
// Optimistic update: reflect UI before server response
const [optimisticTodos, addOptimisticTodo] = useOptimistic(
todos,
(currentTodos, newTodo) => [...currentTodos, { ...newTodo, pending: true }]
);
async function submitTodo(formData) {
const text = formData.get('text');
const tempTodo = { id: Date.now(), text, completed: false };
addOptimisticTodo(tempTodo); // Immediately update UI
await addTodo(text); // Actually save to server (replaced with real data after completion)
}
return (
<div>
<ul>
{optimisticTodos.map(todo => (
<li key={todo.id} style={{ opacity: todo.pending ? 0.5 : 1 }}>
{todo.text} {todo.pending && '(Saving...)'}
</li>
))}
</ul>
<form action={submitTodo}>
<input name="text" placeholder="Enter a task" />
<button type="submit">Add</button>
</form>
</div>
);
}
use() Hook
use() is a new API that can read Promises and Context anywhere inside a component.
Reading a Promise
import { use, Suspense } from 'react';
// Fetch data from server
async function getUser(id) {
const response = await fetch(`/api/users/${id}`);
return response.json();
}
// Read Promise with use() on client
function UserProfile({ userPromise }) {
// Suspends at the Suspense boundary until Promise resolves
const user = use(userPromise); // Acts like await but is a hook
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
}
// Handle loading state with Suspense
function App() {
const userPromise = getUser(1); // Pass Promise directly
return (
<Suspense fallback={<div>Loading...</div>}>
<UserProfile userPromise={userPromise} />
</Suspense>
);
}
// Unlike regular hooks, can be used conditionally
function ConditionalData({ loadData, condition }) {
if (condition) {
const data = use(loadData()); // Can be used conditionally!
return <div>{data.value}</div>;
}
return <div>No data</div>;
}
Reading Context
import { use, createContext } from 'react';
const ThemeContext = createContext('light');
// Read Context with use() (can replace useContext)
function ThemedButton({ children }) {
const theme = use(ThemeContext);
return (
<button className={`btn-${theme}`}>
{children}
</button>
);
}
// Read Context conditionally (advantage of use())
function SmartComponent({ isThemed }) {
if (isThemed) {
const theme = use(ThemeContext); // Can be used conditionally
return <div className={theme}>Theme applied</div>;
}
return <div>Default style</div>;
}
React Compiler (Automatic Memoization)
React Compiler is a build-time optimization tool introduced with React 19. It automatically applies memoization without useMemo, useCallback, or React.memo.
// Before React 18: Manual optimization required
import { useMemo, useCallback, memo } from 'react';
const ExpensiveComponent = memo(function ExpensiveComponent({ data, onAction }) {
const processed = useMemo(() => {
return data.map(item => expensiveProcess(item));
}, [data]);
const handleAction = useCallback((id) => {
onAction(id);
}, [onAction]);
return (
<ul>
{processed.map(item => (
<li key={item.id} onClick={() => handleAction(item.id)}>
{item.value}
</li>
))}
</ul>
);
});
// React 19 + Compiler: Automatic optimization
// Just add 'react-compiler' babel plugin or Next.js config
function ExpensiveComponent({ data, onAction }) {
// Compiler automatically determines optimal memoization placement
const processed = data.map(item => expensiveProcess(item));
const handleAction = (id) => {
onAction(id);
};
return (
<ul>
{processed.map(item => (
<li key={item.id} onClick={() => handleAction(item.id)}>
{item.value}
</li>
))}
</ul>
);
}
// Same performance without memo, useMemo, useCallback!
React Compiler Configuration
// babel.config.js
module.exports = {
plugins: [
['babel-plugin-react-compiler', {
// Compiler configuration
target: '18', // Also compatible with React 18
}]
]
};
// Next.js 15+ (automatic support)
// next.config.js
module.exports = {
experimental: {
reactCompiler: true
}
};
React 19 vs 18 Migration Guide
Summary of Changes
// 1. ref callback cleanup (React 19)
function Component() {
return (
<input
ref={(node) => {
// node: DOM element on mount, null on unmount
if (node) {
node.focus();
}
// React 19: Can return cleanup function
return () => {
// Runs on unmount
console.log('Input field unmounted');
};
}}
/>
);
}
// 2. forwardRef removed (not needed in React 19)
// React 18
import { forwardRef } from 'react';
const Input18 = forwardRef(function Input({ label }, ref) {
return <input ref={ref} placeholder={label} />;
});
// React 19: ref can be passed as a regular prop
function Input19({ label, ref }) {
return <input ref={ref} placeholder={label} />;
}
// 3. Context Provider simplified
// React 18
<ThemeContext.Provider value={theme}>
{children}
</ThemeContext.Provider>
// React 19
<ThemeContext value={theme}>
{children}
</ThemeContext>
// 4. useDeferredValue initial value support
import { useDeferredValue } from 'react';
// React 19: Can specify initial value
const deferredValue = useDeferredValue(value, { initialValue: '' });
Migration Checklist
// Items to check when migrating from React 18 → 19
// 1. ReactDOM.render → createRoot (should already be done)
// Deprecated (removed):
// ReactDOM.render(<App />, document.getElementById('root'));
// Modern:
import { createRoot } from 'react-dom/client';
const root = createRoot(document.getElementById('root'));
root.render(<App />);
// 2. hydrate → hydrateRoot
import { hydrateRoot } from 'react-dom/client';
hydrateRoot(document, <App />);
// 3. Handle useEffect running twice in Strict Mode
// React 18 Strict Mode: In development, runs mount-unmount-mount sequence
// → Ensure cleanup functions are properly implemented
useEffect(() => {
const subscription = subscribeToData(userId);
return () => {
subscription.unsubscribe(); // Cleanup required!
};
}, [userId]);
// 4. Check Concurrent Mode compatibility
// Since rendering can be interrupted/restarted, side effects should only be in useEffect
Practical Example: Server Component + Client Component Combination
// app/dashboard/page.jsx (Server Component)
import { DashboardStats } from './DashboardStats';
import { RecentActivity } from './RecentActivity';
import { InteractiveChart } from './InteractiveChart'; // Client
import { getUserDashboard } from '@/lib/data';
export default async function DashboardPage({ params }) {
// Fetch data directly on the server
const { stats, activity, chartData } = await getUserDashboard(params.userId);
return (
<div className="dashboard">
{/* Server components: static data */}
<DashboardStats stats={stats} />
<RecentActivity activities={activity} />
{/* Client component: interactive */}
<InteractiveChart initialData={chartData} />
</div>
);
}
// app/dashboard/InteractiveChart.jsx (Client Component)
'use client';
import { useState, useTransition } from 'react';
function InteractiveChart({ initialData }) {
const [data, setData] = useState(initialData);
const [isPending, startTransition] = useTransition();
function handlePeriodChange(period) {
startTransition(async () => {
const response = await fetch(`/api/chart?period=${period}`);
const newData = await response.json();
setData(newData);
});
}
return (
<div>
<div className="period-selector">
{['1W', '1M', '3M', '1Y'].map(period => (
<button key={period} onClick={() => handlePeriodChange(period)}>
{period}
</button>
))}
</div>
{isPending ? (
<div className="loading">Loading data...</div>
) : (
<Chart data={data} />
)}
</div>
);
}
Pro Tips
1. Generate Metadata from Server Components
// In Next.js 14+ App Router
export async function generateMetadata({ params }) {
const product = await getProduct(params.id);
return {
title: product.name,
description: product.description,
openGraph: {
images: [product.imageUrl]
}
};
}
export default async function ProductPage({ params }) {
const product = await getProduct(params.id);
return <ProductDetail product={product} />;
}
2. Progressive Loading with Streaming
import { Suspense } from 'react';
async function SlowComponent() {
await new Promise(resolve => setTimeout(resolve, 2000));
const data = await fetchSlowData();
return <div>{data.value}</div>;
}
export default function Page() {
return (
<div>
<h1>Fast Content</h1>
<Suspense fallback={<Skeleton />}>
<SlowComponent /> {/* Streamed after 2 seconds */}
</Suspense>
</div>
);
// Fast content is sent first, SlowComponent streams when ready
}
3. Error Boundary Combined with Server Actions
'use client';
import { useActionState } from 'react';
function Form({ serverAction }) {
const [state, action, isPending] = useActionState(
async (prev, formData) => {
try {
return await serverAction(formData);
} catch (err) {
return { error: err.message };
}
},
null
);
return (
<form action={action}>
{state?.error && <Alert>{state.error}</Alert>}
{/* Form fields */}
<button disabled={isPending}>
{isPending ? <Spinner /> : 'Submit'}
</button>
</form>
);
}