React 19 · How it works
2 min read- How it works
- Table of Contents
- 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
How it works
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
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} />;
}