React 19
12 min read- React 19 - Complete Guide
- Table of Contents
- Overview
- React Compiler
- Before (Manual Memoization)
- After (Compiler Handles It)
- Enabling the Compiler
- Actions
- Basic Action Pattern
- Form Actions
- useActionState
- With Server Actions
- useFormStatus
- Multiple Form Fields
- useOptimistic
- Like Button Example
- use Hook
- Reading Promises
- Conditional Use
- Reading Context Conditionally
- Server Components
- Server Component
- Client Component
- Patterns
- Server Actions
- Document Metadata
- Stylesheets Support
- Async Scripts
- Resource Preloading
- ref as a Prop
- With TypeScript
- Improved Error Reporting
- Hydration Errors
- Error Boundaries
- Context as Provider
- Cleanup Functions for Refs
- useDeferredValue Initial Value
- Questions & Answers
React 19 - Complete Guide
Master all new features in React 19 for senior-level interviews.
Table of Contents
- Overview
- React Compiler
- Actions
- useActionState
- useFormStatus
- useOptimistic
- use Hook
- Server Components
- Server Actions
- Document Metadata
- Stylesheets Support
- Async Scripts
- Resource Preloading
- ref as a Prop
- Improved Error Reporting
- Context as Provider
- Cleanup Functions for Refs
- useDeferredValue Initial Value
- Questions & Answers
Overview
React 19 brings major improvements focusing on:
- Automatic memoization via the React Compiler
- Actions for handling async operations and form submissions
- Server Components first-class support
- Better DX with simplified APIs and improved error messages
React Compiler
The React Compiler (formerly React Forget) automatically memoizes components and hooks, eliminating the need for manual useMemo, useCallback, and React.memo.
Before (Manual Memoization)
// React 18 - manual optimization
function ProductList({ products, onAddToCart }) {
const sortedProducts = useMemo(
() => products.sort((a, b) => a.price - b.price),
[products]
);
const handleAdd = useCallback(
(id) => onAddToCart(id),
[onAddToCart]
);
return (
<ul>
{sortedProducts.map(product => (
<ProductItem
key={product.id}
product={product}
onAdd={handleAdd}
/>
))}
</ul>
);
}
const ProductItem = React.memo(({ product, onAdd }) => (
<li>
{product.name} - ${product.price}
<button onClick={() => onAdd(product.id)}>Add</button>
</li>
));
After (Compiler Handles It)
// React 19 - compiler optimizes automatically
function ProductList({ products, onAddToCart }) {
const sortedProducts = products.sort((a, b) => a.price - b.price);
return (
<ul>
{products.map(product => (
<ProductItem
key={product.id}
product={product}
onAdd={(id) => onAddToCart(id)}
/>
))}
</ul>
);
}
function ProductItem({ product, onAdd }) {
return (
<li>
{product.name} - ${product.price}
<button onClick={() => onAdd(product.id)}>Add</button>
</li>
);
}
Enabling the Compiler
// babel.config.js
module.exports = {
plugins: [
['babel-plugin-react-compiler', {
// Compiler options
}]
]
};
// Or with Vite
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import reactCompiler from 'babel-plugin-react-compiler';
export default defineConfig({
plugins: [
react({
babel: {
plugins: [reactCompiler]
}
})
]
});
Actions
Actions are async functions that handle data mutations, form submissions, and async state transitions. They integrate with Suspense and provide built-in pending states.
Basic Action Pattern
// React 19 action pattern
function UpdateProfile() {
const [error, setError] = useState(null);
const [isPending, startTransition] = useTransition();
const handleSubmit = async (formData) => {
startTransition(async () => {
const result = await updateProfile(formData);
if (result.error) {
setError(result.error);
}
});
};
return (
<form action={handleSubmit}>
<input name="name" />
<button disabled={isPending}>
{isPending ? 'Saving...' : 'Save'}
</button>
{error && <p className="error">{error}</p>}
</form>
);
}
Form Actions
// Direct form action
async function createPost(formData) {
'use server'; // Server Action marker
const title = formData.get('title');
const content = formData.get('content');
await db.posts.create({ title, content });
revalidatePath('/posts');
}
function NewPostForm() {
return (
<form action={createPost}>
<input name="title" placeholder="Title" />
<textarea name="content" placeholder="Content" />
<button type="submit">Create Post</button>
</form>
);
}
useActionState
useActionState (formerly useFormState) manages form state and async actions with automatic pending states.
import { useActionState } from 'react';
async function submitForm(previousState, formData) {
const email = formData.get('email');
if (!email.includes('@')) {
return { error: 'Invalid email address' };
}
await api.subscribe(email);
return { success: true, message: 'Subscribed!' };
}
function NewsletterForm() {
const [state, formAction, isPending] = useActionState(submitForm, null);
return (
<form action={formAction}>
<input
type="email"
name="email"
placeholder="your@email.com"
disabled={isPending}
/>
<button type="submit" disabled={isPending}>
{isPending ? 'Subscribing...' : 'Subscribe'}
</button>
{state?.error && <p className="error">{state.error}</p>}
{state?.success && <p className="success">{state.message}</p>}
</form>
);
}
With Server Actions
// Server Action
async function addToCart(prevState, formData) {
'use server';
const productId = formData.get('productId');
try {
await db.cart.add(productId);
revalidatePath('/cart');
return { success: true };
} catch (error) {
return { error: error.message };
}
}
// Client Component
function AddToCartButton({ productId }) {
const [state, formAction, isPending] = useActionState(addToCart, null);
return (
<form action={formAction}>
<input type="hidden" name="productId" value={productId} />
<button disabled={isPending}>
{isPending ? 'Adding...' : 'Add to Cart'}
</button>
{state?.error && <span className="error">{state.error}</span>}
</form>
);
}
useFormStatus
useFormStatus provides pending state for form submissions from within form components.
import { useFormStatus } from 'react-dom';
function SubmitButton() {
const { pending, data, method, action } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? 'Submitting...' : 'Submit'}
</button>
);
}
function ContactForm() {
async function handleSubmit(formData) {
await api.contact(formData);
}
return (
<form action={handleSubmit}>
<input name="name" placeholder="Name" />
<input name="email" placeholder="Email" />
<textarea name="message" placeholder="Message" />
<SubmitButton /> {/* Automatically knows form pending state */}
</form>
);
}
Multiple Form Fields
function FormFields() {
const { pending } = useFormStatus();
return (
<>
<input name="username" disabled={pending} />
<input name="password" type="password" disabled={pending} />
<SubmitButton />
</>
);
}
function LoginForm({ action }) {
return (
<form action={action}>
<FormFields />
</form>
);
}
useOptimistic
useOptimistic enables optimistic UI updates that revert automatically if the async action fails.
import { useOptimistic } from 'react';
function TodoList({ todos, addTodo }) {
const [optimisticTodos, addOptimisticTodo] = useOptimistic(
todos,
(currentTodos, newTodo) => [...currentTodos, newTodo]
);
async function handleSubmit(formData) {
const title = formData.get('title');
const newTodo = { id: Date.now(), title, pending: true };
// Immediately update UI
addOptimisticTodo(newTodo);
// Actual API call
await addTodo(title);
}
return (
<div>
<ul>
{optimisticTodos.map(todo => (
<li
key={todo.id}
style={{ opacity: todo.pending ? 0.5 : 1 }}
>
{todo.title}
</li>
))}
</ul>
<form action={handleSubmit}>
<input name="title" placeholder="New todo" />
<button type="submit">Add</button>
</form>
</div>
);
}
Like Button Example
function LikeButton({ postId, initialLiked, initialCount }) {
const [liked, setLiked] = useState(initialLiked);
const [count, setCount] = useState(initialCount);
const [optimisticLiked, toggleOptimisticLike] = useOptimistic(
liked,
(current) => !current
);
const [optimisticCount, updateOptimisticCount] = useOptimistic(
count,
(current, change) => current + change
);
async function handleClick() {
const newLiked = !liked;
const change = newLiked ? 1 : -1;
toggleOptimisticLike();
updateOptimisticCount(change);
try {
await api.toggleLike(postId);
setLiked(newLiked);
setCount(count + change);
} catch {
// Optimistic state automatically reverts on error
}
}
return (
<button onClick={handleClick}>
{optimisticLiked ? '❤️' : '🤍'} {optimisticCount}
</button>
);
}
use Hook
The use hook reads resources (Promises, Context) during render. It can be called conditionally, unlike other hooks.
Reading Promises
import { use, Suspense } from 'react';
function UserProfile({ userPromise }) {
// Suspends until promise resolves
const user = use(userPromise);
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}
function App() {
const userPromise = fetchUser(userId);
return (
<Suspense fallback={<div>Loading...</div>}>
<UserProfile userPromise={userPromise} />
</Suspense>
);
}
Conditional Use
function Comments({ commentsPromise, showComments }) {
// Can be called conditionally!
if (!showComments) {
return null;
}
const comments = use(commentsPromise);
return (
<ul>
{comments.map(comment => (
<li key={comment.id}>{comment.text}</li>
))}
</ul>
);
}
Reading Context Conditionally
function ThemeIcon({ forceLight }) {
// Can conditionally read context
if (forceLight) {
return <SunIcon />;
}
const theme = use(ThemeContext);
return theme === 'dark' ? <MoonIcon /> : <SunIcon />;
}
Server Components
React Server Components (RSC) run on the server and stream to the client. They can directly access databases, file systems, and other server resources.
Server Component
// app/posts/page.jsx (Server Component by default)
import { db } from '@/lib/db';
async function PostsPage() {
// Direct database access - no API needed
const posts = await db.posts.findMany({
orderBy: { createdAt: 'desc' }
});
return (
<div>
<h1>Posts</h1>
{posts.map(post => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
<LikeButton postId={post.id} /> {/* Client Component */}
</article>
))}
</div>
);
}
export default PostsPage;
Client Component
// components/LikeButton.jsx
'use client'; // Mark as Client Component
import { useState } from 'react';
export function LikeButton({ postId }) {
const [liked, setLiked] = useState(false);
return (
<button onClick={() => setLiked(!liked)}>
{liked ? '❤️' : '🤍'}
</button>
);
}
Patterns
// Server Component with async data
async function ProductPage({ params }) {
const product = await getProduct(params.id);
const reviews = await getReviews(params.id);
return (
<div>
<ProductDetails product={product} />
<Suspense fallback={<ReviewsSkeleton />}>
<Reviews reviews={reviews} />
</Suspense>
<AddToCartButton productId={params.id} /> {/* Client */}
</div>
);
}
// Passing Server data to Client Components
async function Dashboard() {
const user = await getCurrentUser();
const stats = await getUserStats(user.id);
return (
<DashboardClient
initialUser={user}
initialStats={stats}
/>
);
}
Server Actions
Server Actions are async functions that run on the server, callable from Client Components.
// actions/user.js
'use server';
import { db } from '@/lib/db';
import { revalidatePath } from 'next/cache';
export async function updateProfile(formData) {
const name = formData.get('name');
const bio = formData.get('bio');
await db.user.update({
where: { id: getCurrentUserId() },
data: { name, bio }
});
revalidatePath('/profile');
}
export async function deleteAccount() {
await db.user.delete({
where: { id: getCurrentUserId() }
});
redirect('/');
}
// components/ProfileForm.jsx
'use client';
import { updateProfile } from '@/actions/user';
import { useActionState } from 'react';
export function ProfileForm({ user }) {
const [state, formAction, isPending] = useActionState(
updateProfile,
null
);
return (
<form action={formAction}>
<input name="name" defaultValue={user.name} />
<textarea name="bio" defaultValue={user.bio} />
<button disabled={isPending}>
{isPending ? 'Saving...' : 'Save'}
</button>
</form>
);
}
Document Metadata
React 19 supports rendering <title>, <meta>, and <link> tags directly in components.
function BlogPost({ post }) {
return (
<article>
<title>{post.title} | My Blog</title>
<meta name="description" content={post.excerpt} />
<meta property="og:title" content={post.title} />
<meta property="og:image" content={post.image} />
<link rel="canonical" href={`https://myblog.com/posts/${post.slug}`} />
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
);
}
function ProductPage({ product }) {
return (
<>
<title>{product.name} - Shop</title>
<meta name="description" content={product.description} />
<meta name="robots" content="index, follow" />
<ProductDetails product={product} />
</>
);
}
Stylesheets Support
React 19 adds built-in support for stylesheets with automatic ordering and deduplication.
function Component() {
return (
<>
{/* precedence determines order */}
<link rel="stylesheet" href="/styles/base.css" precedence="default" />
<link rel="stylesheet" href="/styles/theme.css" precedence="high" />
<div className="styled-component">Content</div>
</>
);
}
// Suspense waits for stylesheets
function StyledSection() {
return (
<Suspense fallback={<Skeleton />}>
<link rel="stylesheet" href="/styles/section.css" precedence="default" />
<section>Styled content</section>
</Suspense>
);
}
Async Scripts
React 19 supports async script loading with deduplication.
function AnalyticsComponent() {
return (
<>
<script async src="https://analytics.example.com/script.js" />
<div>Analytics enabled</div>
</>
);
}
function PaymentForm() {
return (
<>
<script async src="https://js.stripe.com/v3/" />
<form>
<div id="card-element" />
<button>Pay</button>
</form>
</>
);
}
Resource Preloading
New APIs for preloading resources to improve performance.
import { prefetchDNS, preconnect, preload, preinit } from 'react-dom';
function App() {
// DNS prefetch for external domains
prefetchDNS('https://api.example.com');
// Preconnect to CDN
preconnect('https://cdn.example.com');
// Preload critical resources
preload('/fonts/inter.woff2', { as: 'font', type: 'font/woff2' });
preload('/hero-image.jpg', { as: 'image' });
// Preinit scripts (load and execute)
preinit('https://analytics.example.com/script.js', { as: 'script' });
return <MainContent />;
}
function ImageGallery({ images }) {
// Preload next image
const nextImage = images[currentIndex + 1];
if (nextImage) {
preload(nextImage.src, { as: 'image' });
}
return <img src={images[currentIndex].src} />;
}
ref as a Prop
Function components can now receive ref as a regular prop, no forwardRef needed.
// React 18 - forwardRef required
const Input = forwardRef((props, ref) => (
<input ref={ref} {...props} />
));
// React 19 - ref is just a prop
function Input({ ref, ...props }) {
return <input ref={ref} {...props} />;
}
// Usage unchanged
function Form() {
const inputRef = useRef(null);
return (
<>
<Input ref={inputRef} placeholder="Enter text" />
<button onClick={() => inputRef.current.focus()}>
Focus
</button>
</>
);
}
With TypeScript
// React 19 - simpler typing
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
ref?: React.Ref<HTMLInputElement>;
}
function Input({ ref, ...props }: InputProps) {
return <input ref={ref} {...props} />;
}
Improved Error Reporting
React 19 provides better error messages and avoids duplicate logging.
Hydration Errors
// React 19 shows clear diff for hydration mismatches
// Error: Hydration mismatch
// Server: <div>Hello World</div>
// Client: <div>Hello Client</div>
Error Boundaries
function ErrorBoundary({ children, fallback }) {
return (
<ErrorBoundaryComponent
fallback={fallback}
onError={(error, errorInfo) => {
// React 19 provides better error info
console.error('Error:', error);
console.error('Component Stack:', errorInfo.componentStack);
}}
>
{children}
</ErrorBoundaryComponent>
);
}
Context as Provider
Context can now be rendered directly as a provider.
// React 18
const ThemeContext = createContext('light');
function App() {
return (
<ThemeContext.Provider value="dark">
<Content />
</ThemeContext.Provider>
);
}
// React 19 - simpler syntax
function App() {
return (
<ThemeContext value="dark">
<Content />
</ThemeContext>
);
}
Cleanup Functions for Refs
Ref callbacks can now return cleanup functions.
// React 18 - manual cleanup
function Component() {
const ref = useCallback((node) => {
if (node) {
const observer = new ResizeObserver(handleResize);
observer.observe(node);
// No way to cleanup!
}
}, []);
return <div ref={ref} />;
}
// React 19 - cleanup function
function Component() {
const ref = useCallback((node) => {
if (!node) return;
const observer = new ResizeObserver(handleResize);
observer.observe(node);
// Cleanup when element is removed
return () => {
observer.disconnect();
};
}, []);
return <div ref={ref} />;
}
useDeferredValue Initial Value
useDeferredValue now accepts an initial value for first render.
// React 18
function SearchResults({ query }) {
const deferredQuery = useDeferredValue(query);
// First render uses query immediately
return <Results query={deferredQuery} />;
}
// React 19 - initial value
function SearchResults({ query }) {
// First render uses '' (shows cached/placeholder)
// Then updates to query
const deferredQuery = useDeferredValue(query, '');
return <Results query={deferredQuery} />;
}
Questions & Answers
A: The React Compiler automatically adds memoization. You don't need to change code, but you can remove manual useMemo, useCallback, and React.memo calls as the compiler handles optimization.
A: useActionState manages the full action lifecycle (state, action function, pending). useFormStatus only provides pending state and must be used inside a form's child component.
A: Use it when you want immediate UI feedback before an async operation completes. It automatically reverts if the action fails.
A: Server Components require a bundler setup. Frameworks like Next.js, Remix, or Waku provide the necessary infrastructure. Manual setup is complex.
A: Server Components are components that render on the server (read data, no interactivity). Server Actions are functions that run on the server and can be called from Client Components (mutations, form handling).
A: No, function components can receive ref as a regular prop. forwardRef still works for backwards compatibility but is no longer needed.
A: use integrates with Suspense - it suspends the component while the promise resolves. It can also be called conditionally, unlike other hooks. It's for rendering, while await is for effects/actions.
A: It was renamed to useActionState in the final React 19 release. The functionality is the same.
A: Actions work best for forms that need server interaction or complex state. Simple client-only forms can continue using controlled components with useState.
A: Return error state from the action and handle it in the component. Use try/catch in the action and return { error: message } that the component can display.