
React 19 Patterns
- 1 installs
- 404 repo stars
- Updated August 5, 2026
- aiskillstore/marketplace
react-19-patterns is a skill that provides React 19 hooks, Server/Client Component, Suspense, and transition patterns with TypeScript.
About
This skill covers React 19 patterns including hooks, Server and Client Components, Suspense, streaming, and transitions with TypeScript. It documents the new React 19 hooks like use(), useOptimistic(), useFormStatus(), and useActionState(). A developer uses it when building React components or migrating from React 18 to 19.
- React 19 hooks, Server and Client Components with TypeScript
- Suspense, streaming, transitions, and optimistic UI
- Bundled validate-react.py to check Rules of Hooks
React 19 Patterns by the numbers
- 1 all-time installs (skills.sh)
- Ranked #1,912 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
react-19-patterns capabilities & compatibility
- Capabilities
- frontend · ui design
- Use cases
- frontend · ui design
- Pricing
- Free
What react-19-patterns says it does
Comprehensive React 19 patterns including all hooks, Server/Client Components, Suspense, streaming, and transitions. Ensures correct React 19 usage with TypeScript.
use()` - For async data in components
npx skills add https://github.com/aiskillstore/marketplace --skill react-19-patternsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1 |
|---|---|
| repo stars | ★ 404 |
| Last updated | August 5, 2026 |
| Repository | aiskillstore/marketplace ↗ |
What it does
Build React 19 components with correct hooks, Server/Client boundaries, Suspense, and transitions.
Who is it for?
Developers building React 19 components or migrating from React 18.
Skip if: Non-React frontends or older React versions without the new hooks.
When should I use this skill?
Writing React components, using React 19 hooks, or migrating from React 18 to 19.
What you get
Produces correct React 19 components with hooks, Suspense, transitions, and optimistic UI.
- React 19 components
- Server Action forms
- Suspense boundaries
By the numbers
- 9 detailed guide files including validate-react.py
- 4 new React 19 hooks documented
Files
React 19 Patterns - Comprehensive Guide
When to Use This Skill
Use this skill when:
- Writing React components (Server or Client)
- Using React hooks (standard or new React 19 hooks)
- Implementing forms with Server Actions
- Working with Suspense and streaming
- Managing state and transitions
- Optimistic UI updates
- Migrating from React 18 to React 19
What This Skill Covers
Core Patterns
- Server vs Client Components - Complete decision tree
- All React Hooks - Complete reference with TypeScript
- Suspense Patterns - Boundaries, streaming, error handling
- Server Components - Data fetching, caching, composition
- Client Components - Interactivity, state, effects
- Transitions - useTransition, startTransition, isPending
- Streaming - Progressive rendering patterns
- Migration Guide - React 18 → React 19
New in React 19
use()- For async data in componentsuseOptimistic()- For optimistic UI updatesuseFormStatus()- For form submission stateuseActionState()- For Server Action state- Enhanced
useTransition()- Better performance - Improved error boundaries
- Better hydration
Quick Reference
Server Component Pattern
// ✅ Default - async data fetching
export default async function ProjectsPage() {
const projects = await db.project.findMany()
return <ProjectList projects={projects} />
}Client Component Pattern
// ✅ Use 'use client' for interactivity
'use client'
import { useState } from 'react'
export function InteractiveComponent() {
const [count, setCount] = useState(0)
return <button onClick={() => setCount(count + 1)}>{count}</button>
}New React 19 Hook Pattern
'use client'
import { useOptimistic } from 'react'
export function TodoList({ todos }: Props) {
const [optimisticTodos, addOptimisticTodo] = useOptimistic(
todos,
(state, newTodo: string) => [...state, { id: 'temp', text: newTodo, pending: true }]
)
return (
<form action={async (formData) => {
addOptimisticTodo(formData.get('todo'))
await createTodo(formData)
}}>
<input name="todo" />
<button type="submit">Add</button>
</form>
)
}File Structure
This skill is organized into detailed guides:
1. server-vs-client.md - Decision tree for component type 2. hooks-complete.md - All React hooks with TypeScript 3. suspense-patterns.md - Suspense boundaries and streaming 4. server-components-complete.md - Server Component patterns 5. client-components-complete.md - Client Component patterns 6. transitions.md - useTransition and concurrent features 7. streaming-patterns.md - Progressive rendering 8. migration-guide.md - React 18 → 19 migration 9. validate-react.py - Validation tool for React rules
Decision Flow
START: Creating new component
│
├─ Does it need interactivity (onClick, onChange)?
│ ├─ YES → Read client-components-complete.md
│ └─ NO → Continue
│
├─ Does it need React hooks (useState, useEffect)?
│ ├─ YES → Read client-components-complete.md + hooks-complete.md
│ └─ NO → Continue
│
├─ Does it fetch data?
│ ├─ YES → Read server-components-complete.md
│ └─ NO → Continue
│
└─ Default → Server Component (read server-components-complete.md)
Need specific hook help?
└─ Read hooks-complete.md (complete reference)
Need Suspense/streaming?
└─ Read suspense-patterns.md + streaming-patterns.md
Need optimistic UI?
└─ Read hooks-complete.md (useOptimistic section)
Need form handling?
└─ Read hooks-complete.md (useFormStatus, useActionState)
Migrating from React 18?
└─ Read migration-guide.mdCommon Mistakes Prevented
❌ Async Client Component
'use client'
export default async function Bad() {} // ERROR!✅ Use Server Component or useEffect
// Option 1: Server Component
export default async function Good() {} // ✅
// Option 2: Client with useEffect
'use client'
export default function Good() {
useEffect(() => {
fetchData()
}, [])
}❌ Hooks in Conditions
if (condition) {
useState(0) // ERROR: Rules of Hooks violation
}✅ Hooks at Top Level
const [value, setValue] = useState(0)
if (condition) {
// Use the hook result here
}❌ Browser APIs in Server Component
export default function Bad() {
const data = localStorage.getItem('key') // ERROR!
return <div>{data}</div>
}✅ Use Client Component
'use client'
export default function Good() {
const [data, setData] = useState(() =>
localStorage.getItem('key')
)
return <div>{data}</div>
}Validation
Use validate-react.py to check your React code:
# Validate single file
python .claude/skills/react-19-patterns/validate-react.py src/components/Button.tsx
# Validate directory
python .claude/skills/react-19-patterns/validate-react.py src/components/
# Auto-fix (where possible)
python .claude/skills/react-19-patterns/validate-react.py --fix src/components/Checks for:
- Rules of Hooks violations
- Server/Client component mistakes
- Missing 'use client' directives
- Invalid async Client Components
- Browser API usage in Server Components
- Non-serializable props to Client Components
Best Practices
1. Default to Server Components
- Better performance (no JS to client)
- Direct data access
- SEO friendly
2. Use Client Components Sparingly
- Only when interactivity needed
- Keep them small
- Minimize bundle size
3. Compose Server + Client
- Fetch data in Server Components
- Pass as props to Client Components
- Best of both worlds
4. Use New React 19 Hooks
use()for async datauseOptimistic()for instant feedbackuseFormStatus()for form statesuseActionState()for Server Actions
5. Leverage Suspense
- Stream data progressively
- Better perceived performance
- Parallel data loading
Resources
- React 19 Docs: https://react.dev/
- Server Components: https://react.dev/reference/rsc/server-components
- React 19 Changelog: https://react.dev/blog/2024/12/05/react-19
- Hooks API: https://react.dev/reference/react/hooks
- Server Actions: https://react.dev/reference/rsc/server-actions
Quick Links
- Server vs Client Decision Tree
- All Hooks Reference
- Suspense Patterns
- Server Components Guide
- Client Components Guide
- Transitions Guide
- Streaming Patterns
- Migration Guide
- Validation Tool
---
Last Updated: 2025-11-23 React Version: 19.2.0 Next.js Version: 15.5
Client Components - Complete Guide
Table of Contents
- Client Components Overview
- 'use client' Directive
- State Management
- Event Handlers
- Form Handling
- Browser APIs
- Third-Party Libraries
- Optimization Techniques
- Code Splitting
- Dynamic Imports
- Bundle Size Optimization
Client Components Overview
Client Components run in the browser. They:
- Hydrate after initial HTML load
- Enable interactivity (onClick, onChange, etc.)
- Support all React hooks
- Can use browser APIs
- Add to JavaScript bundle
When to Use
✅ Use Client Components for:
- Interactive UI (buttons, forms, modals)
- React hooks (useState, useEffect, useContext)
- Event handlers (onClick, onChange, onSubmit)
- Browser APIs (localStorage, window, geolocation)
- Client-side libraries (animation, charts)
❌ Don't Use for:
- Static content (use Server Components)
- Data fetching (prefer Server Components)
- SEO-critical content (use Server Components)
'use client' Directive
Placement Rules
// ✅ CORRECT: Top of file, before imports
'use client'
import { useState } from 'react'
export function Component() {
const [state, setState] = useState(0)
return <div>{state}</div>
}// ❌ WRONG: After imports
import { useState } from 'react'
'use client' // ERROR: Must be at top
export function Component() {}File-Level Directive
// components/Button.tsx
'use client'
// All exports are Client Components
export function Button() {
return <button onClick={() => alert('Hi')}>Click</button>
}
export function IconButton() {
return <button onClick={() => alert('Icon')}>🎉</button>
}Boundary Marking
// ✅ Only mark the boundary
// components/InteractiveSection.tsx
'use client'
import { PureComponent } from './PureComponent' // Also becomes client!
export function InteractiveSection() {
const [open, setOpen] = useState(false)
return (
<div>
<button onClick={() => setOpen(!open)}>Toggle</button>
{open && <PureComponent />}
</div>
)
}State Management
useState Patterns
'use client'
import { useState } from 'react'
// Simple state
export function Counter() {
const [count, setCount] = useState(0)
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
<button onClick={() => setCount(count - 1)}>Decrement</button>
<button onClick={() => setCount(0)}>Reset</button>
</div>
)
}
// Object state
interface FormState {
name: string
email: string
age: number
}
export function Form() {
const [form, setForm] = useState<FormState>({
name: '',
email: '',
age: 0,
})
const updateField = <K extends keyof FormState>(
field: K,
value: FormState[K]
) => {
setForm((prev) => ({ ...prev, [field]: value }))
}
return (
<form>
<input
value={form.name}
onChange={(e) => updateField('name', e.target.value)}
/>
<input
value={form.email}
onChange={(e) => updateField('email', e.target.value)}
/>
<input
type="number"
value={form.age}
onChange={(e) => updateField('age', parseInt(e.target.value))}
/>
</form>
)
}
// Array state
export function TodoList() {
const [todos, setTodos] = useState<string[]>([])
const [input, setInput] = useState('')
const addTodo = () => {
setTodos((prev) => [...prev, input])
setInput('')
}
const removeTodo = (index: number) => {
setTodos((prev) => prev.filter((_, i) => i !== index))
}
return (
<div>
<input value={input} onChange={(e) => setInput(e.target.value)} />
<button onClick={addTodo}>Add</button>
<ul>
{todos.map((todo, i) => (
<li key={i}>
{todo}
<button onClick={() => removeTodo(i)}>Delete</button>
</li>
))}
</ul>
</div>
)
}useReducer for Complex State
'use client'
import { useReducer } from 'react'
interface State {
count: number
step: number
}
type Action =
| { type: 'increment' }
| { type: 'decrement' }
| { type: 'reset' }
| { type: 'setStep'; step: number }
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'increment':
return { ...state, count: state.count + state.step }
case 'decrement':
return { ...state, count: state.count - state.step }
case 'reset':
return { ...state, count: 0 }
case 'setStep':
return { ...state, step: action.step }
default:
return state
}
}
export function AdvancedCounter() {
const [state, dispatch] = useReducer(reducer, { count: 0, step: 1 })
return (
<div>
<p>Count: {state.count}</p>
<p>Step: {state.step}</p>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
<button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
<input
type="number"
value={state.step}
onChange={(e) =>
dispatch({ type: 'setStep', step: parseInt(e.target.value) })
}
/>
</div>
)
}Context for Global State
'use client'
import { createContext, useContext, useState, ReactNode } from 'react'
interface User {
id: string
name: string
email: string
}
interface AuthContextType {
user: User | null
login: (user: User) => void
logout: () => void
}
const AuthContext = createContext<AuthContextType | undefined>(undefined)
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null)
const login = (user: User) => setUser(user)
const logout = () => setUser(null)
return (
<AuthContext.Provider value={{ user, login, logout }}>
{children}
</AuthContext.Provider>
)
}
export function useAuth() {
const context = useContext(AuthContext)
if (!context) {
throw new Error('useAuth must be used within AuthProvider')
}
return context
}
// Usage
export function LoginButton() {
const { user, login, logout } = useAuth()
if (user) {
return (
<div>
<span>Welcome, {user.name}</span>
<button onClick={logout}>Logout</button>
</div>
)
}
return (
<button
onClick={() =>
login({ id: '1', name: 'Alice', email: 'alice@example.com' })
}
>
Login
</button>
)
}Event Handlers
Basic Event Handlers
'use client'
export function InteractiveComponent() {
// Click events
const handleClick = () => {
console.log('Clicked!')
}
// Mouse events
const handleMouseEnter = () => {
console.log('Mouse entered')
}
// Keyboard events
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
console.log('Enter pressed')
}
}
// Focus events
const handleFocus = () => {
console.log('Focused')
}
return (
<div>
<button onClick={handleClick}>Click Me</button>
<div onMouseEnter={handleMouseEnter}>Hover Me</div>
<input onKeyDown={handleKeyDown} onFocus={handleFocus} />
</div>
)
}Event Delegation
'use client'
export function List({ items }: { items: string[] }) {
const handleItemClick = (e: React.MouseEvent) => {
const target = e.target as HTMLElement
const index = target.dataset.index
console.log('Clicked item:', index)
}
return (
<ul onClick={handleItemClick}>
{items.map((item, i) => (
<li key={i} data-index={i}>
{item}
</li>
))}
</ul>
)
}Synthetic Events
'use client'
export function FormWithEvents() {
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault() // Prevent default form submission
const formData = new FormData(e.currentTarget)
const data = Object.fromEntries(formData)
console.log('Form data:', data)
}
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
console.log('Input changed:', e.target.value)
}
return (
<form onSubmit={handleSubmit}>
<input name="username" onChange={handleChange} />
<button type="submit">Submit</button>
</form>
)
}Form Handling
Controlled Components
'use client'
import { useState } from 'react'
export function ControlledForm() {
const [formData, setFormData] = useState({
name: '',
email: '',
message: '',
})
const handleChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
) => {
const { name, value } = e.target
setFormData((prev) => ({ ...prev, [name]: value }))
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
const response = await fetch('/api/contact', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData),
})
if (response.ok) {
alert('Form submitted!')
setFormData({ name: '', email: '', message: '' })
}
}
return (
<form onSubmit={handleSubmit}>
<input
name="name"
value={formData.name}
onChange={handleChange}
required
/>
<input
name="email"
type="email"
value={formData.email}
onChange={handleChange}
required
/>
<textarea
name="message"
value={formData.message}
onChange={handleChange}
required
/>
<button type="submit">Submit</button>
</form>
)
}With Validation
'use client'
import { useState } from 'react'
interface Errors {
name?: string
email?: string
}
export function ValidatedForm() {
const [formData, setFormData] = useState({ name: '', email: '' })
const [errors, setErrors] = useState<Errors>({})
const validate = (): boolean => {
const newErrors: Errors = {}
if (!formData.name) {
newErrors.name = 'Name is required'
}
if (!formData.email) {
newErrors.email = 'Email is required'
} else if (!/\S+@\S+\.\S+/.test(formData.email)) {
newErrors.email = 'Email is invalid'
}
setErrors(newErrors)
return Object.keys(newErrors).length === 0
}
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
if (validate()) {
console.log('Form is valid:', formData)
}
}
return (
<form onSubmit={handleSubmit}>
<div>
<input
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
/>
{errors.name && <span className="error">{errors.name}</span>}
</div>
<div>
<input
type="email"
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
/>
{errors.email && <span className="error">{errors.email}</span>}
</div>
<button type="submit">Submit</button>
</form>
)
}React 19 useFormStatus
'use client'
import { useFormStatus } from 'react-dom'
function SubmitButton() {
const { pending } = useFormStatus()
return (
<button type="submit" disabled={pending}>
{pending ? 'Submitting...' : 'Submit'}
</button>
)
}
export function ModernForm() {
async function handleSubmit(formData: FormData) {
// Server Action
await new Promise((r) => setTimeout(r, 2000))
console.log('Submitted:', formData.get('name'))
}
return (
<form action={handleSubmit}>
<input name="name" required />
<SubmitButton />
</form>
)
}Browser APIs
LocalStorage
'use client'
import { useState, useEffect } from 'react'
export function useLocalStorage<T>(key: string, initialValue: T) {
const [value, setValue] = useState<T>(() => {
if (typeof window === 'undefined') return initialValue
const saved = localStorage.getItem(key)
return saved ? JSON.parse(saved) : initialValue
})
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value))
}, [key, value])
return [value, setValue] as const
}
// Usage
export function ThemeToggle() {
const [theme, setTheme] = useLocalStorage('theme', 'light')
return (
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Theme: {theme}
</button>
)
}Geolocation
'use client'
import { useState, useEffect } from 'react'
interface Position {
latitude: number
longitude: number
}
export function LocationComponent() {
const [position, setPosition] = useState<Position | null>(null)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
if (!navigator.geolocation) {
setError('Geolocation not supported')
return
}
navigator.geolocation.getCurrentPosition(
(pos) => {
setPosition({
latitude: pos.coords.latitude,
longitude: pos.coords.longitude,
})
},
(err) => {
setError(err.message)
}
)
}, [])
if (error) return <div>Error: {error}</div>
if (!position) return <div>Loading location...</div>
return (
<div>
Latitude: {position.latitude}, Longitude: {position.longitude}
</div>
)
}IntersectionObserver
'use client'
import { useEffect, useRef, useState } from 'react'
export function LazyImage({ src, alt }: { src: string; alt: string }) {
const [isVisible, setIsVisible] = useState(false)
const imgRef = useRef<HTMLImageElement>(null)
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsVisible(true)
observer.disconnect()
}
},
{ threshold: 0.1 }
)
if (imgRef.current) {
observer.observe(imgRef.current)
}
return () => observer.disconnect()
}, [])
return (
<img
ref={imgRef}
src={isVisible ? src : '/placeholder.png'}
alt={alt}
loading="lazy"
/>
)
}Third-Party Libraries
Animation Libraries
'use client'
import { motion } from 'framer-motion'
export function AnimatedCard() {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.3 }}
>
Card Content
</motion.div>
)
}Chart Libraries
'use client'
import { Line } from 'react-chartjs-2'
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
PointElement,
LineElement,
} from 'chart.js'
ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement)
export function LineChart({ data }: { data: number[] }) {
const chartData = {
labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May'],
datasets: [
{
label: 'Sales',
data: data,
borderColor: 'rgb(75, 192, 192)',
},
],
}
return <Line data={chartData} />
}Optimization Techniques
React.memo
'use client'
import { memo } from 'react'
interface Props {
name: string
count: number
}
// Only re-renders when props change
export const ExpensiveComponent = memo(function ExpensiveComponent({
name,
count,
}: Props) {
console.log('ExpensiveComponent rendered')
return (
<div>
{name}: {count}
</div>
)
})useCallback
'use client'
import { useCallback, memo } from 'react'
const Child = memo(({ onClick }: { onClick: () => void }) => {
console.log('Child rendered')
return <button onClick={onClick}>Click</button>
})
export function Parent() {
const [count, setCount] = useState(0)
// ❌ Without useCallback: Child re-renders every time
// const handleClick = () => setCount(count + 1)
// ✅ With useCallback: Child doesn't re-render
const handleClick = useCallback(() => {
setCount((c) => c + 1)
}, [])
return (
<div>
<p>Count: {count}</p>
<Child onClick={handleClick} />
</div>
)
}useMemo
'use client'
import { useMemo } from 'react'
export function ExpensiveList({ items }: { items: number[] }) {
// ❌ Without useMemo: Computes on every render
// const sorted = items.slice().sort((a, b) => b - a)
// ✅ With useMemo: Only recomputes when items change
const sorted = useMemo(() => {
console.log('Sorting items...')
return items.slice().sort((a, b) => b - a)
}, [items])
return (
<ul>
{sorted.map((item, i) => (
<li key={i}>{item}</li>
))}
</ul>
)
}Code Splitting
Dynamic Imports
'use client'
import { lazy, Suspense } from 'react'
// Lazy load heavy component
const HeavyComponent = lazy(() => import('./HeavyComponent'))
export function Page() {
return (
<div>
<h1>Page</h1>
<Suspense fallback={<div>Loading...</div>}>
<HeavyComponent />
</Suspense>
</div>
)
}Conditional Loading
'use client'
import { useState, lazy, Suspense } from 'react'
const Modal = lazy(() => import('./Modal'))
export function App() {
const [showModal, setShowModal] = useState(false)
return (
<div>
<button onClick={() => setShowModal(true)}>Open Modal</button>
{showModal && (
<Suspense fallback={<div>Loading modal...</div>}>
<Modal onClose={() => setShowModal(false)} />
</Suspense>
)}
</div>
)
}Bundle Size Optimization
Tree Shaking
// ❌ BAD: Imports entire library
import _ from 'lodash'
const result = _.uniq([1, 2, 2, 3])
// ✅ GOOD: Import only what's needed
import uniq from 'lodash/uniq'
const result = uniq([1, 2, 2, 3])Dynamic Imports for Heavy Libraries
'use client'
import { useState } from 'react'
export function ChartComponent() {
const [data, setData] = useState([])
const loadChart = async () => {
// Only load when needed
const { Chart } = await import('chart.js')
// Use Chart
}
return <button onClick={loadChart}>Load Chart</button>
}---
Next: Read transitions.md for transition patterns.
React Hooks - Complete Reference (React 19)
Table of Contents
- Rules of Hooks
- State Hooks
- Effect Hooks
- Ref Hooks
- Context Hooks
- Performance Hooks
- Transition Hooks
- New React 19 Hooks
- Custom Hooks
Rules of Hooks
These rules are MANDATORY and enforced by ESLint:
Rule 1: Only Call Hooks at the Top Level
❌ DON'T call hooks inside conditions, loops, or nested functions:
// ❌ BAD: Hook in condition
function BadComponent() {
if (condition) {
const [state, setState] = useState(0) // ERROR!
}
}
// ❌ BAD: Hook in loop
function BadComponent() {
for (let i = 0; i < 10; i++) {
const [state, setState] = useState(0) // ERROR!
}
}
// ❌ BAD: Hook in nested function
function BadComponent() {
function nested() {
const [state, setState] = useState(0) // ERROR!
}
}✅ DO call hooks at the top level:
// ✅ GOOD: Hooks at top level
function GoodComponent() {
const [state, setState] = useState(0) // ✅
const [other, setOther] = useState('') // ✅
if (condition) {
// Use the hook results here
setState(1)
}
return <div>{state}</div>
}Rule 2: Only Call Hooks in React Functions
❌ DON'T call hooks in regular JavaScript functions:
// ❌ BAD: Hook in regular function
function regularFunction() {
const [state, setState] = useState(0) // ERROR!
}✅ DO call hooks in React components or custom hooks:
// ✅ GOOD: Hook in component
function MyComponent() {
const [state, setState] = useState(0) // ✅
return <div>{state}</div>
}
// ✅ GOOD: Hook in custom hook
function useCustomHook() {
const [state, setState] = useState(0) // ✅
return [state, setState] as const
}State Hooks
useState
The fundamental hook for managing component state.
Basic Usage
import { useState } from 'react'
function Counter() {
const [count, setCount] = useState(0)
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
<button onClick={() => setCount(count - 1)}>Decrement</button>
</div>
)
}With TypeScript
// Primitive type (inferred)
const [count, setCount] = useState(0) // number
// Explicit type
const [name, setName] = useState<string>('')
// Union type
const [status, setStatus] = useState<'idle' | 'loading' | 'error'>('idle')
// Object type
interface User {
id: string
name: string
email: string
}
const [user, setUser] = useState<User | null>(null)
// Array type
const [items, setItems] = useState<string[]>([])Functional Updates
Use functional updates when new state depends on previous state:
function Counter() {
const [count, setCount] = useState(0)
// ❌ BAD: May be stale
const increment = () => {
setCount(count + 1)
setCount(count + 1) // Will only increment by 1!
}
// ✅ GOOD: Functional update
const increment = () => {
setCount((prev) => prev + 1)
setCount((prev) => prev + 1) // Will increment by 2!
}
return <button onClick={increment}>Count: {count}</button>
}Lazy Initialization
Use lazy initialization for expensive computations:
// ❌ BAD: Runs on every render
function Component() {
const [state, setState] = useState(expensiveComputation())
}
// ✅ GOOD: Runs only once
function Component() {
const [state, setState] = useState(() => expensiveComputation())
}
// Example: localStorage
function Component() {
const [user, setUser] = useState<User | null>(() => {
const saved = localStorage.getItem('user')
return saved ? JSON.parse(saved) : null
})
}Complex State Objects
interface FormState {
name: string
email: string
age: number
}
function Form() {
const [form, setForm] = useState<FormState>({
name: '',
email: '',
age: 0,
})
// Update single field
const updateName = (name: string) => {
setForm((prev) => ({ ...prev, name }))
}
// Generic field updater
const updateField = (field: keyof FormState, value: any) => {
setForm((prev) => ({ ...prev, [field]: value }))
}
return (
<form>
<input
value={form.name}
onChange={(e) => updateName(e.target.value)}
/>
<input
value={form.email}
onChange={(e) => updateField('email', e.target.value)}
/>
</form>
)
}useReducer
Alternative to useState for complex state logic.
Basic Usage
import { useReducer } from 'react'
type State = { count: number }
type Action = { type: 'increment' } | { type: 'decrement' } | { type: 'reset' }
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'increment':
return { count: state.count + 1 }
case 'decrement':
return { count: state.count - 1 }
case 'reset':
return { count: 0 }
default:
return state
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, { count: 0 })
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
<button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
</div>
)
}Complex Example: Todo List
interface Todo {
id: string
text: string
completed: boolean
}
type State = {
todos: Todo[]
filter: 'all' | 'active' | 'completed'
}
type Action =
| { type: 'add'; text: string }
| { type: 'toggle'; id: string }
| { type: 'delete'; id: string }
| { type: 'setFilter'; filter: State['filter'] }
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'add':
return {
...state,
todos: [
...state.todos,
{ id: crypto.randomUUID(), text: action.text, completed: false },
],
}
case 'toggle':
return {
...state,
todos: state.todos.map((todo) =>
todo.id === action.id
? { ...todo, completed: !todo.completed }
: todo
),
}
case 'delete':
return {
...state,
todos: state.todos.filter((todo) => todo.id !== action.id),
}
case 'setFilter':
return {
...state,
filter: action.filter,
}
default:
return state
}
}
function TodoApp() {
const [state, dispatch] = useReducer(reducer, {
todos: [],
filter: 'all',
})
const filteredTodos = state.todos.filter((todo) => {
if (state.filter === 'active') return !todo.completed
if (state.filter === 'completed') return todo.completed
return true
})
return (
<div>
<input
onKeyDown={(e) => {
if (e.key === 'Enter') {
dispatch({ type: 'add', text: e.currentTarget.value })
e.currentTarget.value = ''
}
}}
/>
<ul>
{filteredTodos.map((todo) => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => dispatch({ type: 'toggle', id: todo.id })}
/>
{todo.text}
<button onClick={() => dispatch({ type: 'delete', id: todo.id })}>
Delete
</button>
</li>
))}
</ul>
</div>
)
}When to Use useReducer vs useState
✅ Use useReducer when:
- Multiple state values that change together
- Complex state updates
- State logic is complex (multiple actions)
- Want to extract state logic (testable)
✅ Use useState when:
- Simple, independent state
- Single value
- Simple updates
Effect Hooks
useEffect
Runs side effects after render.
Basic Usage
import { useEffect } from 'react'
function Component() {
useEffect(() => {
// Effect code runs after render
console.log('Component mounted or updated')
// Cleanup function (optional)
return () => {
console.log('Component unmounted or before next effect')
}
})
return <div>Component</div>
}Dependency Array
// ❌ NO dependency array: Runs after EVERY render
useEffect(() => {
console.log('Every render')
})
// ✅ Empty array: Runs ONCE (mount only)
useEffect(() => {
console.log('Mount only')
}, [])
// ✅ With dependencies: Runs when dependencies change
useEffect(() => {
console.log('Count changed:', count)
}, [count])
// ✅ Multiple dependencies
useEffect(() => {
console.log('Count or name changed')
}, [count, name])Cleanup Function
// Event listener cleanup
useEffect(() => {
const handleResize = () => {
console.log('Window resized')
}
window.addEventListener('resize', handleResize)
return () => {
window.removeEventListener('resize', handleResize)
}
}, [])
// Timer cleanup
useEffect(() => {
const timer = setInterval(() => {
console.log('Tick')
}, 1000)
return () => clearInterval(timer)
}, [])
// Subscription cleanup
useEffect(() => {
const subscription = dataSource.subscribe(() => {
// Handle data
})
return () => subscription.unsubscribe()
}, [])Common Patterns
Data Fetching:
function UserProfile({ userId }: { userId: string }) {
const [user, setUser] = useState<User | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<Error | null>(null)
useEffect(() => {
let cancelled = false
async function fetchUser() {
try {
setLoading(true)
const response = await fetch(`/api/users/${userId}`)
const data = await response.json()
if (!cancelled) {
setUser(data)
setError(null)
}
} catch (err) {
if (!cancelled) {
setError(err instanceof Error ? err : new Error('Unknown error'))
}
} finally {
if (!cancelled) {
setLoading(false)
}
}
}
fetchUser()
return () => {
cancelled = true
}
}, [userId])
if (loading) return <div>Loading...</div>
if (error) return <div>Error: {error.message}</div>
if (!user) return null
return <div>{user.name}</div>
}Local Storage Sync:
function useLocalStorage<T>(key: string, initialValue: T) {
const [value, setValue] = useState<T>(() => {
const saved = localStorage.getItem(key)
return saved ? JSON.parse(saved) : initialValue
})
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value))
}, [key, value])
return [value, setValue] as const
}
// Usage
function Component() {
const [theme, setTheme] = useLocalStorage('theme', 'light')
return <div>Theme: {theme}</div>
}Document Title:
function useDocumentTitle(title: string) {
useEffect(() => {
document.title = title
}, [title])
}
// Usage
function Page() {
useDocumentTitle('My Page Title')
return <div>Content</div>
}useLayoutEffect
Same as useEffect but fires synchronously after DOM mutations.
When to Use
✅ Use useLayoutEffect when:
- Measuring DOM elements
- Synchronous DOM mutations before paint
- Preventing visual flicker
⚠️ WARNING: Blocks visual updates, use sparingly!
Example: Measuring DOM
import { useLayoutEffect, useRef, useState } from 'react'
function MeasuredComponent() {
const ref = useRef<HTMLDivElement>(null)
const [height, setHeight] = useState(0)
useLayoutEffect(() => {
if (ref.current) {
setHeight(ref.current.getBoundingClientRect().height)
}
}, [])
return (
<div>
<div ref={ref}>Content to measure</div>
<p>Height: {height}px</p>
</div>
)
}Example: Preventing Flicker
function TooltipPosition({ children }: { children: React.ReactNode }) {
const ref = useRef<HTMLDivElement>(null)
useLayoutEffect(() => {
if (ref.current) {
const rect = ref.current.getBoundingClientRect()
// Reposition if off-screen
if (rect.right > window.innerWidth) {
ref.current.style.right = '0'
ref.current.style.left = 'auto'
}
}
})
return <div ref={ref} className="tooltip">{children}</div>
}Ref Hooks
useRef
Creates a mutable ref object.
DOM References
import { useRef, useEffect } from 'react'
function AutoFocusInput() {
const inputRef = useRef<HTMLInputElement>(null)
useEffect(() => {
// Focus input on mount
inputRef.current?.focus()
}, [])
return <input ref={inputRef} />
}Mutable Values
function Timer() {
const [count, setCount] = useState(0)
const intervalRef = useRef<number | null>(null)
const start = () => {
if (intervalRef.current !== null) return
intervalRef.current = window.setInterval(() => {
setCount((c) => c + 1)
}, 1000)
}
const stop = () => {
if (intervalRef.current !== null) {
clearInterval(intervalRef.current)
intervalRef.current = null
}
}
useEffect(() => {
return () => stop()
}, [])
return (
<div>
<p>Count: {count}</p>
<button onClick={start}>Start</button>
<button onClick={stop}>Stop</button>
</div>
)
}Previous Value
function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T>()
useEffect(() => {
ref.current = value
}, [value])
return ref.current
}
// Usage
function Component({ count }: { count: number }) {
const prevCount = usePrevious(count)
return (
<div>
Current: {count}, Previous: {prevCount}
</div>
)
}Imperative Handle (Advanced)
import { useImperativeHandle, forwardRef, useRef } from 'react'
interface InputHandle {
focus: () => void
clear: () => void
}
const CustomInput = forwardRef<InputHandle, {}>((props, ref) => {
const inputRef = useRef<HTMLInputElement>(null)
useImperativeHandle(ref, () => ({
focus: () => inputRef.current?.focus(),
clear: () => {
if (inputRef.current) inputRef.current.value = ''
},
}))
return <input ref={inputRef} />
})
// Usage
function Parent() {
const inputRef = useRef<InputHandle>(null)
return (
<div>
<CustomInput ref={inputRef} />
<button onClick={() => inputRef.current?.focus()}>Focus</button>
<button onClick={() => inputRef.current?.clear()}>Clear</button>
</div>
)
}Context Hooks
useContext
Reads context value.
Basic Usage
import { createContext, useContext, ReactNode } from 'react'
// Define context type
interface ThemeContextType {
theme: 'light' | 'dark'
setTheme: (theme: 'light' | 'dark') => void
}
// Create context
const ThemeContext = createContext<ThemeContextType | undefined>(undefined)
// Provider component
function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setTheme] = useState<'light' | 'dark'>('light')
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
)
}
// Custom hook for consuming context
function useTheme() {
const context = useContext(ThemeContext)
if (context === undefined) {
throw new Error('useTheme must be used within ThemeProvider')
}
return context
}
// Consumer component
function ThemedButton() {
const { theme, setTheme } = useTheme()
return (
<button
className={theme}
onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}
>
Toggle Theme
</button>
)
}Multiple Contexts
interface User {
id: string
name: string
}
const UserContext = createContext<User | null>(null)
const ThemeContext = createContext<'light' | 'dark'>('light')
function App() {
return (
<UserContext.Provider value={{ id: '1', name: 'Alice' }}>
<ThemeContext.Provider value="dark">
<Dashboard />
</ThemeContext.Provider>
</UserContext.Provider>
)
}
function Dashboard() {
const user = useContext(UserContext)
const theme = useContext(ThemeContext)
return (
<div className={theme}>
{user ? `Welcome, ${user.name}` : 'Not logged in'}
</div>
)
}Performance Hooks
useMemo
Memoizes expensive computations.
When to Use
✅ Use when:
- Expensive computations
- Referential equality matters
- Preventing child re-renders
❌ Don't use when:
- Simple computations
- Premature optimization
Basic Usage
import { useMemo } from 'react'
function ExpensiveComponent({ items }: { items: string[] }) {
// ❌ BAD: Computes on every render
const sorted = items.slice().sort()
// ✅ GOOD: Only recomputes when items change
const sorted = useMemo(() => {
return items.slice().sort()
}, [items])
return (
<ul>
{sorted.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
)
}Complex Example
interface Product {
id: string
name: string
price: number
category: string
}
function ProductList({ products, filter }: Props) {
const filteredAndSorted = useMemo(() => {
console.log('Computing filtered and sorted products')
return products
.filter((p) => p.category === filter)
.sort((a, b) => a.price - b.price)
}, [products, filter])
return (
<ul>
{filteredAndSorted.map((product) => (
<ProductItem key={product.id} product={product} />
))}
</ul>
)
}useCallback
Memoizes function references.
When to Use
✅ Use when:
- Passing callbacks to memoized children
- Dependencies in useEffect
- Referential equality matters
❌ Don't use when:
- Function not passed to children
- Premature optimization
Basic Usage
import { useCallback } from 'react'
function Parent() {
const [count, setCount] = useState(0)
// ❌ BAD: New function on every render
const increment = () => setCount(count + 1)
// ✅ GOOD: Stable function reference
const increment = useCallback(() => {
setCount((c) => c + 1)
}, [])
return <Child onIncrement={increment} />
}
const Child = React.memo(({ onIncrement }: Props) => {
console.log('Child rendered')
return <button onClick={onIncrement}>Increment</button>
})With Dependencies
function SearchComponent({ onSearch }: { onSearch: (query: string) => void }) {
const [query, setQuery] = useState('')
const handleSearch = useCallback(() => {
if (query.trim()) {
onSearch(query)
}
}, [query, onSearch])
return (
<div>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<button onClick={handleSearch}>Search</button>
</div>
)
}Transition Hooks
useTransition
Marks state updates as non-urgent (See transitions.md for complete guide).
import { useTransition } from 'react'
function SearchResults() {
const [query, setQuery] = useState('')
const [results, setResults] = useState([])
const [isPending, startTransition] = useTransition()
const handleSearch = (value: string) => {
// Urgent: Update input immediately
setQuery(value)
// Non-urgent: Update results in background
startTransition(() => {
const filtered = searchData(value)
setResults(filtered)
})
}
return (
<div>
<input value={query} onChange={(e) => handleSearch(e.target.value)} />
{isPending && <Spinner />}
<ResultsList results={results} />
</div>
)
}useDeferredValue
Defers updating a value.
import { useDeferredValue } from 'react'
function SearchResults({ query }: { query: string }) {
const deferredQuery = useDeferredValue(query)
// deferredQuery lags behind query
const results = useMemo(() => {
return searchData(deferredQuery)
}, [deferredQuery])
return (
<div>
<p>Searching for: {query}</p>
{query !== deferredQuery && <Spinner />}
<ResultsList results={results} />
</div>
)
}useId
Generates unique IDs for accessibility.
import { useId } from 'react'
function FormField({ label }: { label: string }) {
const id = useId()
return (
<div>
<label htmlFor={id}>{label}</label>
<input id={id} />
</div>
)
}New React 19 Hooks
use (React 19)
Reads promises and context in render.
import { use } from 'react'
// With promises
async function fetchUser(id: string): Promise<User> {
const response = await fetch(`/api/users/${id}`)
return response.json()
}
function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
// use() unwraps the promise
const user = use(userPromise)
return <div>{user.name}</div>
}
// With context
function ThemedButton() {
const theme = use(ThemeContext)
return <button className={theme}>Click</button>
}useOptimistic (React 19)
Optimistic UI updates.
import { useOptimistic } from 'react'
interface Todo {
id: string
text: string
pending?: boolean
}
function TodoList({ todos }: { todos: Todo[] }) {
const [optimisticTodos, addOptimisticTodo] = useOptimistic(
todos,
(state, newTodo: string) => [
...state,
{ id: `temp-${Date.now()}`, text: newTodo, pending: true },
]
)
async function createTodo(formData: FormData) {
const text = formData.get('todo') as string
addOptimisticTodo(text)
await fetch('/api/todos', {
method: 'POST',
body: JSON.stringify({ text }),
})
}
return (
<div>
<form action={createTodo}>
<input name="todo" />
<button type="submit">Add</button>
</form>
<ul>
{optimisticTodos.map((todo) => (
<li key={todo.id} className={todo.pending ? 'pending' : ''}>
{todo.text}
</li>
))}
</ul>
</div>
)
}useFormStatus (React 19)
Form submission status.
'use client'
import { useFormStatus } from 'react-dom'
function SubmitButton() {
const { pending, data, method, action } = useFormStatus()
return (
<button type="submit" disabled={pending}>
{pending ? 'Submitting...' : 'Submit'}
</button>
)
}
function MyForm() {
async function handleSubmit(formData: FormData) {
await new Promise((resolve) => setTimeout(resolve, 2000))
console.log('Submitted:', formData.get('name'))
}
return (
<form action={handleSubmit}>
<input name="name" />
<SubmitButton />
</form>
)
}useActionState (React 19)
Manages Server Action state.
'use client'
import { useActionState } from 'react'
interface FormState {
message: string
errors?: Record<string, string[]>
}
async function createProject(
prevState: FormState,
formData: FormData
): Promise<FormState> {
const name = formData.get('name') as string
if (!name) {
return {
message: 'Validation failed',
errors: { name: ['Name is required'] },
}
}
try {
await fetch('/api/projects', {
method: 'POST',
body: JSON.stringify({ name }),
})
return { message: 'Project created successfully' }
} catch (error) {
return { message: 'Failed to create project' }
}
}
function CreateProjectForm() {
const [state, formAction] = useActionState(createProject, {
message: '',
})
return (
<form action={formAction}>
<input name="name" />
{state.errors?.name && <p className="error">{state.errors.name[0]}</p>}
<button type="submit">Create</button>
{state.message && <p>{state.message}</p>}
</form>
)
}Custom Hooks
Rules for Custom Hooks
1. Name must start with use 2. Can call other hooks 3. Should be reusable 4. Return values or functions
Example: useDebounce
function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState(value)
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedValue(value)
}, delay)
return () => clearTimeout(timer)
}, [value, delay])
return debouncedValue
}
// Usage
function SearchInput() {
const [query, setQuery] = useState('')
const debouncedQuery = useDebounce(query, 500)
useEffect(() => {
if (debouncedQuery) {
// Search with debounced value
search(debouncedQuery)
}
}, [debouncedQuery])
return <input value={query} onChange={(e) => setQuery(e.target.value)} />
}Example: useAsync
interface AsyncState<T> {
data: T | null
loading: boolean
error: Error | null
}
function useAsync<T>(
asyncFunction: () => Promise<T>,
dependencies: any[] = []
): AsyncState<T> {
const [state, setState] = useState<AsyncState<T>>({
data: null,
loading: true,
error: null,
})
useEffect(() => {
let cancelled = false
setState({ data: null, loading: true, error: null })
asyncFunction()
.then((data) => {
if (!cancelled) {
setState({ data, loading: false, error: null })
}
})
.catch((error) => {
if (!cancelled) {
setState({ data: null, loading: false, error })
}
})
return () => {
cancelled = true
}
}, dependencies)
return state
}
// Usage
function UserProfile({ userId }: { userId: string }) {
const { data: user, loading, error } = useAsync(
() => fetch(`/api/users/${userId}`).then((r) => r.json()),
[userId]
)
if (loading) return <div>Loading...</div>
if (error) return <div>Error: {error.message}</div>
if (!user) return null
return <div>{user.name}</div>
}Example: useMediaQuery
function useMediaQuery(query: string): boolean {
const [matches, setMatches] = useState(false)
useEffect(() => {
const media = window.matchMedia(query)
setMatches(media.matches)
const listener = (e: MediaQueryListEvent) => setMatches(e.matches)
media.addEventListener('change', listener)
return () => media.removeEventListener('change', listener)
}, [query])
return matches
}
// Usage
function ResponsiveComponent() {
const isMobile = useMediaQuery('(max-width: 768px)')
const isDesktop = useMediaQuery('(min-width: 1024px)')
return (
<div>
{isMobile && <MobileView />}
{isDesktop && <DesktopView />}
</div>
)
}Example: useOnClickOutside
function useOnClickOutside<T extends HTMLElement>(
ref: React.RefObject<T>,
handler: (event: MouseEvent | TouchEvent) => void
) {
useEffect(() => {
const listener = (event: MouseEvent | TouchEvent) => {
if (!ref.current || ref.current.contains(event.target as Node)) {
return
}
handler(event)
}
document.addEventListener('mousedown', listener)
document.addEventListener('touchstart', listener)
return () => {
document.removeEventListener('mousedown', listener)
document.removeEventListener('touchstart', listener)
}
}, [ref, handler])
}
// Usage
function Modal({ onClose }: { onClose: () => void }) {
const modalRef = useRef<HTMLDivElement>(null)
useOnClickOutside(modalRef, onClose)
return (
<div ref={modalRef} className="modal">
Modal content
</div>
)
}---
Next: Read suspense-patterns.md for Suspense and streaming patterns.
React 18 → React 19 Migration Guide
Overview
React 19 includes breaking changes and new features. This guide helps you migrate from React 18.x to React 19.x.
Breaking Changes
1. Removed Legacy APIs
ReactDOM.render (Removed)
// ❌ React 18 (deprecated)
import ReactDOM from 'react-dom'
ReactDOM.render(<App />, document.getElementById('root'))
// ✅ React 19 (must use createRoot)
import { createRoot } from 'react-dom/client'
const root = createRoot(document.getElementById('root')!)
root.render(<App />)ReactDOM.hydrate (Removed)
// ❌ React 18 (deprecated)
import ReactDOM from 'react-dom'
ReactDOM.hydrate(<App />, document.getElementById('root'))
// ✅ React 19 (must use hydrateRoot)
import { hydrateRoot } from 'react-dom/client'
hydrateRoot(document.getElementById('root')!, <App />)2. String Refs (Removed)
// ❌ React 18 (deprecated)
class Component extends React.Component {
componentDidMount() {
this.refs.input.focus() // String ref
}
render() {
return <input ref="input" />
}
}
// ✅ React 19 (use callback or createRef)
class Component extends React.Component {
inputRef = React.createRef<HTMLInputElement>()
componentDidMount() {
this.inputRef.current?.focus()
}
render() {
return <input ref={this.inputRef} />
}
}
// ✅ React 19 (function components)
function Component() {
const inputRef = useRef<HTMLInputElement>(null)
useEffect(() => {
inputRef.current?.focus()
}, [])
return <input ref={inputRef} />
}3. defaultProps (Removed for Function Components)
// ❌ React 18
function Button({ color = 'blue', size = 'medium' }) {
return <button>{/* ... */}</button>
}
Button.defaultProps = {
color: 'blue',
size: 'medium',
}
// ✅ React 19 (use default parameters)
function Button({ color = 'blue', size = 'medium' }) {
return <button>{/* ... */}</button>
}
// ✅ React 19 (with TypeScript)
interface ButtonProps {
color?: string
size?: 'small' | 'medium' | 'large'
}
function Button({ color = 'blue', size = 'medium' }: ButtonProps) {
return <button>{/* ... */}</button>
}4. Context.Provider Pattern Change
// ❌ React 18
const ThemeContext = React.createContext('light')
function App() {
return (
<ThemeContext.Provider value="dark">
<Component />
</ThemeContext.Provider>
)
}
// ✅ React 19 (simplified - just Context works)
const ThemeContext = React.createContext('light')
function App() {
return (
<ThemeContext value="dark">
<Component />
</ThemeContext>
)
}
// Both patterns work in React 19, but simplified is preferred5. Automatic Batching (Now Default)
// React 18: Manual batching needed outside React events
import { unstable_batchedUpdates } from 'react-dom'
function handleClick() {
fetch('/api/data').then(() => {
unstable_batchedUpdates(() => {
setCount((c) => c + 1)
setFlag((f) => !f)
})
})
}
// ✅ React 19: Automatic batching everywhere
function handleClick() {
fetch('/api/data').then(() => {
// Automatically batched!
setCount((c) => c + 1)
setFlag((f) => !f)
})
}New Features in React 19
1. use() Hook
// ❌ React 18: Manual promise handling
function Component() {
const [data, setData] = useState(null)
useEffect(() => {
fetchData().then(setData)
}, [])
if (!data) return <Loading />
return <div>{data}</div>
}
// ✅ React 19: use() hook
import { use } from 'react'
function Component({ dataPromise }: { dataPromise: Promise<Data> }) {
const data = use(dataPromise)
return <div>{data}</div>
}
// Wrap with Suspense
function Page() {
const dataPromise = fetchData()
return (
<Suspense fallback={<Loading />}>
<Component dataPromise={dataPromise} />
</Suspense>
)
}2. useOptimistic() Hook
// ❌ React 18: Manual optimistic updates
function TodoList({ todos }: Props) {
const [optimisticTodos, setOptimisticTodos] = useState(todos)
async function addTodo(text: string) {
// Optimistically add
const tempTodo = { id: 'temp', text, pending: true }
setOptimisticTodos([...optimisticTodos, tempTodo])
try {
const newTodo = await createTodo(text)
// Replace temp with real
setOptimisticTodos((prev) =>
prev.map((t) => (t.id === 'temp' ? newTodo : t))
)
} catch (error) {
// Revert on error
setOptimisticTodos(todos)
}
}
}
// ✅ React 19: useOptimistic hook
import { useOptimistic } from 'react'
function TodoList({ todos }: Props) {
const [optimisticTodos, addOptimisticTodo] = useOptimistic(
todos,
(state, newTodo: string) => [
...state,
{ id: 'temp', text: newTodo, pending: true },
]
)
async function addTodo(formData: FormData) {
const text = formData.get('text') as string
addOptimisticTodo(text)
await createTodo(text)
}
return (
<form action={addTodo}>
<input name="text" />
<button type="submit">Add</button>
<ul>
{optimisticTodos.map((todo) => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
</form>
)
}3. useFormStatus() Hook
// ❌ React 18: Manual form state tracking
function Form() {
const [pending, setPending] = useState(false)
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setPending(true)
try {
await submitForm(new FormData(e.currentTarget))
} finally {
setPending(false)
}
}
return (
<form onSubmit={handleSubmit}>
<input name="name" />
<button disabled={pending}>
{pending ? 'Submitting...' : 'Submit'}
</button>
</form>
)
}
// ✅ React 19: useFormStatus hook
import { useFormStatus } from 'react-dom'
function SubmitButton() {
const { pending } = useFormStatus()
return (
<button type="submit" disabled={pending}>
{pending ? 'Submitting...' : 'Submit'}
</button>
)
}
function Form() {
async function handleSubmit(formData: FormData) {
await submitForm(formData)
}
return (
<form action={handleSubmit}>
<input name="name" />
<SubmitButton />
</form>
)
}4. useActionState() Hook
// ❌ React 18: Manual action state
function Form() {
const [state, setState] = useState({ message: '', errors: {} })
const [pending, setPending] = useState(false)
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setPending(true)
try {
const result = await submitAction(new FormData(e.currentTarget))
setState(result)
} finally {
setPending(false)
}
}
return (
<form onSubmit={handleSubmit}>
<input name="name" />
{state.errors.name && <p>{state.errors.name}</p>}
<button disabled={pending}>Submit</button>
{state.message && <p>{state.message}</p>}
</form>
)
}
// ✅ React 19: useActionState hook
import { useActionState } from 'react'
async function submitAction(prevState: State, formData: FormData): Promise<State> {
const name = formData.get('name') as string
if (!name) {
return {
message: 'Validation failed',
errors: { name: 'Name is required' },
}
}
try {
await saveData(name)
return { message: 'Success!' }
} catch (error) {
return { message: 'Error occurred' }
}
}
function Form() {
const [state, formAction] = useActionState(submitAction, {
message: '',
})
return (
<form action={formAction}>
<input name="name" />
{state.errors?.name && <p>{state.errors.name}</p>}
<button type="submit">Submit</button>
{state.message && <p>{state.message}</p>}
</form>
)
}5. Enhanced Transitions
// React 18: Basic transitions
import { useTransition } from 'react'
function Component() {
const [isPending, startTransition] = useTransition()
const handleClick = () => {
startTransition(() => {
setTab('new-tab')
})
}
}
// ✅ React 19: Enhanced with better interruption
import { useTransition } from 'react'
function Component() {
const [isPending, startTransition] = useTransition()
const handleClick = () => {
startTransition(() => {
// Better interruption handling
// Automatically cancels previous transition
setTab('new-tab')
})
}
}Migration Steps
Step 1: Update Dependencies
# Update to React 19
npm install react@19 react-dom@19
# Update TypeScript types (if using TypeScript)
npm install --save-dev @types/react@19 @types/react-dom@19
# Update Next.js (if using Next.js)
npm install next@15Step 2: Update Root Rendering
// src/index.tsx (React 18)
import ReactDOM from 'react-dom'
import App from './App'
ReactDOM.render(<App />, document.getElementById('root'))
// src/index.tsx (React 19)
import { createRoot } from 'react-dom/client'
import App from './App'
const root = createRoot(document.getElementById('root')!)
root.render(<App />)Step 3: Remove String Refs
# Find all string refs
grep -r 'ref="' src/
# Replace with useRef or createRefStep 4: Remove defaultProps
// Find components with defaultProps
// Replace with default parameters
// Before
function Button({ color, size }) {
return <button>{/* ... */}</button>
}
Button.defaultProps = { color: 'blue', size: 'medium' }
// After
function Button({ color = 'blue', size = 'medium' }) {
return <button>{/* ... */}</button>
}Step 5: Adopt New Features
// 1. Use use() for async data
const data = use(dataPromise)
// 2. Use useOptimistic for optimistic UI
const [optimisticState, addOptimistic] = useOptimistic(state, updater)
// 3. Use useFormStatus for form state
const { pending } = useFormStatus()
// 4. Use useActionState for server actions
const [state, formAction] = useActionState(action, initialState)TypeScript Changes
Type Updates
// React 19 has improved types
// ✅ Better inference for refs
const inputRef = useRef<HTMLInputElement>(null)
// No need for null check in some cases
// ✅ Better event types
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
// e.target is properly typed
}
// ✅ Better children types
interface Props {
children: React.ReactNode // Preferred over React.ReactElement
}Common Issues and Solutions
Issue 1: Hydration Mismatch
// ❌ Problem: Different content on server and client
function Component() {
return <div>{Date.now()}</div>
}
// ✅ Solution: Use useEffect for client-only code
function Component() {
const [time, setTime] = useState<number | null>(null)
useEffect(() => {
setTime(Date.now())
}, [])
return <div>{time ?? 'Loading...'}</div>
}Issue 2: Context Provider Warning
// ⚠️ Warning: Using Context.Provider still works but is deprecated
// ✅ Update to new pattern (optional)
<ThemeContext value="dark">
<App />
</ThemeContext>Issue 3: StrictMode Double Rendering
// React 19 StrictMode intentionally double-renders in development
// ✅ Ensure effects are idempotent
useEffect(() => {
const subscription = subscribe()
return () => {
subscription.unsubscribe() // Cleanup properly
}
}, [])Performance Improvements
1. Automatic Batching
React 19 batches all updates automatically:
// React 18: Only batched in React events
// React 19: Batched everywhere (including promises, setTimeout)
fetch('/api/data').then(() => {
setCount(1)
setFlag(true)
// Both updates batched into single render in React 19
})2. Better Concurrent Rendering
// React 19 has improved concurrent rendering
// Use Suspense more aggressively
<Suspense fallback={<Skeleton />}>
<ExpensiveComponent />
</Suspense>
// Use transitions for non-urgent updates
const [isPending, startTransition] = useTransition()
startTransition(() => {
setResults(expensiveFilter(query))
})Testing Updates
Update Test Setup
// Before (React 18)
import { render } from '@testing-library/react'
test('component', () => {
const { container } = render(<Component />)
// Tests
})
// After (React 19) - Same API!
import { render } from '@testing-library/react'
test('component', () => {
const { container } = render(<Component />)
// Tests work the same
})Test Async Components
// Test Server Components with Suspense
import { render, waitFor } from '@testing-library/react'
test('async component', async () => {
const { getByText } = render(
<Suspense fallback={<div>Loading...</div>}>
<AsyncComponent />
</Suspense>
)
expect(getByText('Loading...')).toBeInTheDocument()
await waitFor(() => {
expect(getByText('Content')).toBeInTheDocument()
})
})Checklist
- [ ] Update react and react-dom to version 19
- [ ] Update @types/react and @types/react-dom to version 19
- [ ] Replace ReactDOM.render with createRoot
- [ ] Replace ReactDOM.hydrate with hydrateRoot
- [ ] Remove all string refs
- [ ] Remove defaultProps from function components
- [ ] Update tests if needed
- [ ] Adopt new hooks (use, useOptimistic, useFormStatus, useActionState)
- [ ] Test application thoroughly
- [ ] Update CI/CD if needed
---
Congratulations! You've migrated to React 19. Explore the new hooks and concurrent features to improve your app's performance and user experience.
Server Components - Complete Guide
Table of Contents
- Server Components Overview
- Basic Async Server Components
- Data Fetching Patterns
- Database Queries
- API Calls
- Parallel Data Fetching
- Sequential Data Fetching
- Caching Strategies
- Revalidation Patterns
- Composition Patterns
- Props to Client Components
- Children Pattern
- Context Limitations
Server Components Overview
Server Components execute only on the server:
- During build time (static generation)
- On each request (dynamic rendering)
- Never send JavaScript to client
- Can access backend resources directly
Benefits
1. Zero Client Bundle - No JS shipped for Server Components 2. Direct Data Access - Query databases, read files 3. Security - Keep secrets on server 4. SEO - Fully rendered HTML 5. Performance - Heavy work on server
Limitations
Cannot use:
- React hooks (useState, useEffect, etc.)
- Exception:
use()in React 19 - Event handlers (onClick, onChange, etc.)
- Browser APIs (window, localStorage, etc.)
- React Context (as provider or consumer)
Can use:
async/awaitfunctions- Node.js APIs (fs, path, crypto, etc.)
- Server-only libraries
- All environment variables
- Database clients
Basic Async Server Components
Simple Async Component
// ✅ Server Component - async by default
export default async function ProjectsPage() {
const projects = await fetchProjects()
return (
<div>
<h1>Projects</h1>
<ul>
{projects.map((project) => (
<li key={project.id}>{project.name}</li>
))}
</ul>
</div>
)
}With TypeScript Types
interface Project {
id: string
name: string
description: string
createdAt: Date
}
async function fetchProjects(): Promise<Project[]> {
const response = await fetch('https://api.example.com/projects')
return response.json()
}
export default async function ProjectsPage() {
const projects: Project[] = await fetchProjects()
return (
<ul>
{projects.map((project: Project) => (
<li key={project.id}>
<h2>{project.name}</h2>
<p>{project.description}</p>
</li>
))}
</ul>
)
}Error Handling
export default async function ProjectsPage() {
try {
const projects = await fetchProjects()
return (
<ul>
{projects.map((project) => (
<li key={project.id}>{project.name}</li>
))}
</ul>
)
} catch (error) {
return (
<div className="error">
<h2>Failed to load projects</h2>
<p>{error instanceof Error ? error.message : 'Unknown error'}</p>
</div>
)
}
}Data Fetching Patterns
Pattern 1: Direct Fetch
async function fetchUser(id: string) {
const response = await fetch(`https://api.example.com/users/${id}`, {
next: { revalidate: 3600 }, // Cache for 1 hour
})
if (!response.ok) {
throw new Error('Failed to fetch user')
}
return response.json()
}
export default async function UserProfile({ params }: { params: { id: string } }) {
const user = await fetchUser(params.id)
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
)
}Pattern 2: With Loading State (Suspense)
// app/projects/page.tsx
import { Suspense } from 'react'
export default function ProjectsPage() {
return (
<div>
<h1>Projects</h1>
<Suspense fallback={<ProjectsSkeleton />}>
<ProjectsList />
</Suspense>
</div>
)
}
async function ProjectsList() {
const projects = await fetchProjects()
return (
<ul>
{projects.map((project) => (
<li key={project.id}>{project.name}</li>
))}
</ul>
)
}
function ProjectsSkeleton() {
return (
<div className="animate-pulse">
{[...Array(5)].map((_, i) => (
<div key={i} className="h-16 bg-gray-200 rounded mb-2"></div>
))}
</div>
)
}Pattern 3: Nested Data Loading
export default async function ProjectPage({ params }: { params: { id: string } }) {
const project = await fetchProject(params.id)
return (
<div>
<h1>{project.name}</h1>
<Suspense fallback={<CommentsSkeleton />}>
<Comments projectId={project.id} />
</Suspense>
</div>
)
}
async function Comments({ projectId }: { projectId: string }) {
const comments = await fetchComments(projectId)
return (
<ul>
{comments.map((comment) => (
<li key={comment.id}>{comment.text}</li>
))}
</ul>
)
}Database Queries
With Drizzle ORM
// lib/db.ts
import { drizzle } from 'drizzle-orm/postgres-js'
import postgres from 'postgres'
const connectionString = process.env.DATABASE_URL!
const client = postgres(connectionString)
export const db = drizzle(client)
// schema.ts
import { pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core'
export const projects = pgTable('projects', {
id: uuid('id').defaultRandom().primaryKey(),
name: text('name').notNull(),
description: text('description'),
createdAt: timestamp('created_at').defaultNow(),
})// app/projects/page.tsx
import { db } from '@/lib/db'
import { projects } from '@/lib/schema'
import { desc } from 'drizzle-orm'
export default async function ProjectsPage() {
const allProjects = await db
.select()
.from(projects)
.orderBy(desc(projects.createdAt))
return (
<ul>
{allProjects.map((project) => (
<li key={project.id}>{project.name}</li>
))}
</ul>
)
}Complex Queries
import { db } from '@/lib/db'
import { projects, users, tasks } from '@/lib/schema'
import { eq, and, gte } from 'drizzle-orm'
export default async function Dashboard() {
// Join query
const projectsWithOwners = await db
.select({
projectId: projects.id,
projectName: projects.name,
ownerName: users.name,
})
.from(projects)
.leftJoin(users, eq(projects.ownerId, users.id))
// Filtered query
const recentProjects = await db
.select()
.from(projects)
.where(
and(
eq(projects.status, 'active'),
gte(projects.createdAt, new Date('2024-01-01'))
)
)
return (
<div>
<ProjectsList projects={projectsWithOwners} />
<RecentProjects projects={recentProjects} />
</div>
)
}Transactions
export default async function CreateProjectPage() {
async function createProjectWithTasks(formData: FormData) {
'use server'
const name = formData.get('name') as string
const taskNames = formData.getAll('tasks') as string[]
await db.transaction(async (tx) => {
// Create project
const [project] = await tx
.insert(projects)
.values({ name })
.returning()
// Create tasks
await tx.insert(tasks).values(
taskNames.map((taskName) => ({
projectId: project.id,
name: taskName,
}))
)
})
revalidatePath('/projects')
}
return (
<form action={createProjectWithTasks}>
<input name="name" required />
<input name="tasks" />
<input name="tasks" />
<button type="submit">Create</button>
</form>
)
}API Calls
Basic API Call
async function fetchGitHubUser(username: string) {
const response = await fetch(`https://api.github.com/users/${username}`, {
headers: {
Authorization: `Bearer ${process.env.GITHUB_TOKEN}`,
},
next: { revalidate: 3600 }, // Cache for 1 hour
})
if (!response.ok) {
throw new Error('Failed to fetch user')
}
return response.json()
}
export default async function GitHubProfile({ username }: { username: string }) {
const user = await fetchGitHubUser(username)
return (
<div>
<img src={user.avatar_url} alt={user.name} />
<h1>{user.name}</h1>
<p>{user.bio}</p>
</div>
)
}With Request Headers
export default async function WeatherPage() {
const response = await fetch('https://api.weather.com/current', {
headers: {
'X-API-Key': process.env.WEATHER_API_KEY!,
'Content-Type': 'application/json',
},
})
const weather = await response.json()
return (
<div>
<h1>Current Weather</h1>
<p>Temperature: {weather.temp}°F</p>
</div>
)
}POST Requests
async function createProject(data: { name: string; description: string }) {
const response = await fetch('https://api.example.com/projects', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${process.env.API_TOKEN}`,
},
body: JSON.stringify(data),
})
if (!response.ok) {
throw new Error('Failed to create project')
}
return response.json()
}Parallel Data Fetching
Promise.all Pattern
export default async function Dashboard() {
// ✅ GOOD: Parallel fetching
const [projects, users, stats] = await Promise.all([
fetchProjects(),
fetchUsers(),
fetchStats(),
])
return (
<div>
<h1>Dashboard</h1>
<ProjectsSection projects={projects} />
<UsersSection users={users} />
<StatsSection stats={stats} />
</div>
)
}Suspense Boundaries for Independent Loading
export default function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
{/* Each section loads independently (parallel) */}
<Suspense fallback={<ProjectsSkeleton />}>
<ProjectsSection />
</Suspense>
<Suspense fallback={<UsersSkeleton />}>
<UsersSection />
</Suspense>
<Suspense fallback={<StatsSkeleton />}>
<StatsSection />
</Suspense>
</div>
)
}
async function ProjectsSection() {
const projects = await fetchProjects()
return <div>{/* render projects */}</div>
}
async function UsersSection() {
const users = await fetchUsers()
return <div>{/* render users */}</div>
}
async function StatsSection() {
const stats = await fetchStats()
return <div>{/* render stats */}</div>
}Sequential Data Fetching
When Data Depends on Previous Data
export default async function ProjectDetails({ params }: { params: { id: string } }) {
// Step 1: Fetch project
const project = await fetchProject(params.id)
// Step 2: Fetch related data (depends on project)
const [owner, tasks] = await Promise.all([
fetchUser(project.ownerId),
fetchTasks(project.id),
])
return (
<div>
<h1>{project.name}</h1>
<p>Owner: {owner.name}</p>
<TasksList tasks={tasks} />
</div>
)
}Waterfall (Avoid When Possible)
// ❌ BAD: Sequential waterfall (slow)
export default async function BadDashboard() {
const projects = await fetchProjects() // Wait 1s
const users = await fetchUsers() // Wait 1s
const stats = await fetchStats() // Wait 1s
// Total: 3 seconds
return <div>{/* ... */}</div>
}
// ✅ GOOD: Parallel loading (fast)
export default async function GoodDashboard() {
const [projects, users, stats] = await Promise.all([
fetchProjects(),
fetchUsers(),
fetchStats(),
])
// Total: 1 second (all parallel)
return <div>{/* ... */}</div>
}Caching Strategies
Next.js Fetch Caching
// Cache forever (until revalidate or rebuild)
fetch('https://api.example.com/data', {
cache: 'force-cache', // Default
})
// Never cache
fetch('https://api.example.com/data', {
cache: 'no-store',
})
// Revalidate after 60 seconds
fetch('https://api.example.com/data', {
next: { revalidate: 60 },
})
// Tag-based revalidation
fetch('https://api.example.com/data', {
next: { tags: ['projects'] },
})Route Segment Config
// app/projects/page.tsx
// Revalidate page every 60 seconds
export const revalidate = 60
// Never cache (always dynamic)
export const dynamic = 'force-dynamic'
// Static generation (cache forever)
export const dynamic = 'force-static'
export default async function ProjectsPage() {
const projects = await fetchProjects()
return <div>{/* ... */}</div>
}generateStaticParams for Dynamic Routes
// app/projects/[id]/page.tsx
export async function generateStaticParams() {
const projects = await fetchProjects()
return projects.map((project) => ({
id: project.id,
}))
}
export default async function ProjectPage({ params }: { params: { id: string } }) {
const project = await fetchProject(params.id)
return <div>{project.name}</div>
}Revalidation Patterns
Time-Based Revalidation
// Revalidate every 60 seconds
export const revalidate = 60
export default async function NewsPage() {
const news = await fetchNews()
return <div>{/* ... */}</div>
}On-Demand Revalidation
// app/api/revalidate/route.ts
import { revalidatePath, revalidateTag } from 'next/cache'
import { NextRequest, NextResponse } from 'next/server'
export async function POST(request: NextRequest) {
const { path, tag } = await request.json()
if (path) {
revalidatePath(path)
}
if (tag) {
revalidateTag(tag)
}
return NextResponse.json({ revalidated: true })
}// app/actions.ts
'use server'
import { revalidatePath } from 'next/cache'
export async function createProject(formData: FormData) {
const name = formData.get('name') as string
await db.insert(projects).values({ name })
// Revalidate projects page
revalidatePath('/projects')
}Tag-Based Revalidation
// Fetch with tags
async function fetchProjects() {
const response = await fetch('https://api.example.com/projects', {
next: { tags: ['projects'] },
})
return response.json()
}
// Revalidate by tag
'use server'
import { revalidateTag } from 'next/cache'
export async function createProject(formData: FormData) {
// Create project...
// Revalidate all fetches tagged with 'projects'
revalidateTag('projects')
}Composition Patterns
Server Component with Client Child
// app/projects/page.tsx (Server Component)
import { ProjectList } from '@/components/ProjectList'
export default async function ProjectsPage() {
const projects = await fetchProjects()
return (
<div>
<h1>Projects</h1>
{/* Pass data to Client Component */}
<ProjectList projects={projects} />
</div>
)
}// components/ProjectList.tsx (Client Component)
'use client'
import { useState } from 'react'
export function ProjectList({ projects }: { projects: Project[] }) {
const [filter, setFilter] = useState('')
const filtered = projects.filter((p) =>
p.name.toLowerCase().includes(filter.toLowerCase())
)
return (
<div>
<input
value={filter}
onChange={(e) => setFilter(e.target.value)}
placeholder="Filter..."
/>
<ul>
{filtered.map((project) => (
<li key={project.id}>{project.name}</li>
))}
</ul>
</div>
)
}Multiple Levels of Composition
// Page (Server Component)
export default async function Page() {
const data = await fetchData()
return (
<div>
<ServerHeader data={data} />
<ClientInteractive data={data}>
<ServerContent data={data} />
</ClientInteractive>
</div>
)
}
// Server Component
async function ServerHeader({ data }: Props) {
return <header>{data.title}</header>
}
// Client Component (can have Server Component children!)
'use client'
function ClientInteractive({ data, children }: Props) {
const [expanded, setExpanded] = useState(false)
return (
<div>
<button onClick={() => setExpanded(!expanded)}>Toggle</button>
{expanded && children}
</div>
)
}
// Server Component
async function ServerContent({ data }: Props) {
return <div>{data.content}</div>
}Props to Client Components
Serializable Props Only
// ✅ GOOD: Serializable props
<ClientComponent
string="hello"
number={42}
boolean={true}
array={[1, 2, 3]}
object={{ id: 1, name: 'Alice' }}
date={new Date()} // Serialized to string
/>
// ❌ BAD: Non-serializable props
<ClientComponent
onClick={() => {}} // ERROR: Function not serializable
user={new User()} // ERROR: Class instance not serializable
/>Complex Data Structures
interface Project {
id: string
name: string
owner: {
id: string
name: string
}
tasks: Array<{
id: string
title: string
completed: boolean
}>
}
export default async function ProjectPage({ params }: { params: { id: string } }) {
const project: Project = await fetchProject(params.id)
// ✅ All serializable
return <ProjectDetails project={project} />
}Children Pattern
Passing Server Components as Children
// ClientWrapper.tsx (Client Component)
'use client'
import { ReactNode } from 'react'
export function ClientWrapper({ children }: { children: ReactNode }) {
const [isOpen, setIsOpen] = useState(true)
return (
<div>
<button onClick={() => setIsOpen(!isOpen)}>Toggle</button>
{isOpen && children}
</div>
)
}
// Page.tsx (Server Component)
export default async function Page() {
const data = await fetchData()
return (
<ClientWrapper>
{/* This is still a Server Component! */}
<ServerContent data={data} />
</ClientWrapper>
)
}
async function ServerContent({ data }: Props) {
// Can do async operations, database queries, etc.
return <div>{data}</div>
}Slots Pattern
// Layout.tsx (Client Component)
'use client'
interface LayoutProps {
header: ReactNode
sidebar: ReactNode
main: ReactNode
}
export function Layout({ header, sidebar, main }: LayoutProps) {
return (
<div className="grid">
<header>{header}</header>
<aside>{sidebar}</aside>
<main>{main}</main>
</div>
)
}
// Page.tsx (Server Component)
export default async function Page() {
const [headerData, sidebarData, mainData] = await Promise.all([
fetchHeader(),
fetchSidebar(),
fetchMain(),
])
return (
<Layout
header={<Header data={headerData} />}
sidebar={<Sidebar data={sidebarData} />}
main={<Main data={mainData} />}
/>
)
}Context Limitations
Cannot Provide Context in Server Components
// ❌ BAD: Context Provider in Server Component
export default async function Layout({ children }: Props) {
return (
<ThemeContext.Provider value="dark">
{children}
</ThemeContext.Provider>
)
}
// ERROR: Context not available in Server ComponentsWorkaround: Wrap with Client Component
// providers.tsx (Client Component)
'use client'
import { ReactNode } from 'react'
export function Providers({ children }: { children: ReactNode }) {
return (
<ThemeContext.Provider value="dark">
{children}
</ThemeContext.Provider>
)
}
// layout.tsx (Server Component)
import { Providers } from './providers'
export default async function RootLayout({ children }: Props) {
return (
<html>
<body>
<Providers>
{/* Children can still be Server Components */}
{children}
</Providers>
</body>
</html>
)
}---
Next: Read client-components-complete.md for Client Component patterns.
Server vs Client Components - Complete Decision Guide
Decision Tree
┌─────────────────────────────────────────────────────────────┐
│ Creating a New React Component │
└───────────────────┬─────────────────────────────────────────┘
│
▼
┌───────────────────────────────────────┐
│ Does it need interactivity? │
│ (onClick, onChange, onSubmit, etc.) │
└───────────┬───────────────────────────┘
│
┌───────┴───────┐
│ │
YES NO
│ │
▼ ▼
CLIENT ┌───────────────────────────┐
COMPONENT │ Does it need React hooks? │
│ (useState, useEffect...) │
└───────┬───────────────────┘
│
┌───────┴───────┐
│ │
YES NO
│ │
▼ ▼
CLIENT ┌───────────────────────────┐
COMPONENT │ Does it use browser APIs? │
│ (window, localStorage...) │
└───────┬───────────────────┘
│
┌───────┴───────┐
│ │
YES NO
│ │
▼ ▼
CLIENT ┌─────────────────────┐
COMPONENT │ Does it fetch data? │
└───────┬─────────────┘
│
┌───────┴───────┐
│ │
YES NO
│ │
▼ ▼
SERVER SERVER
COMPONENT COMPONENT
(preferred) (default)Server Components
What Are Server Components?
Server Components run only on the server. They:
- Execute during the build (static) or on each request (dynamic)
- Never send JavaScript to the client
- Can directly access backend resources
- Support async/await for data fetching
- Are the default in Next.js App Router
Benefits
1. Zero Bundle Size
- No JavaScript sent to client
- Smaller bundle = faster load time
- Better performance on low-end devices
2. Direct Backend Access
- Query databases directly
- Access file system
- Use server-only libraries
- Read environment variables (without NEXT_PUBLIC_)
3. Better Security
- Keep secrets on server
- API keys never exposed
- No client-side code to inspect
4. SEO Friendly
- Fully rendered HTML sent to browser
- Search engines can index content
- No hydration delay
5. Automatic Code Splitting
- Only needed code sent to client
- Reduces initial bundle
- Faster page loads
Limitations
Cannot use:
- React hooks (useState, useEffect, useContext, etc.)
- Exception:
use()hook in React 19 - Event handlers (onClick, onChange, etc.)
- Browser APIs (window, document, localStorage, etc.)
- Lifecycle methods
- React Context (must use Client Components)
Can use:
async/await- Node.js APIs (fs, path, etc.)
- Server-only libraries (database clients, etc.)
- Environment variables (all of them)
When to Use Server Components
✅ USE for:
- Data fetching from databases
- Rendering static content
- SEO-critical pages
- Heavy computations (do on server)
- Accessing backend resources
- Reading files
- Server-side only operations
Examples
Example 1: Database Query
// app/projects/page.tsx
import { db } from '@/lib/db'
// ✅ Server Component (default)
export default async function ProjectsPage() {
// Direct database access
const projects = await db.project.findMany({
where: { status: 'active' },
include: { owner: true },
orderBy: { createdAt: 'desc' },
})
return (
<div>
<h1>Projects</h1>
<ProjectList projects={projects} />
</div>
)
}Example 2: Environment Variables
// app/api-status/page.tsx
// ✅ Server Component - access ALL env vars
export default async function ApiStatusPage() {
const apiKey = process.env.OPENAI_API_KEY // No NEXT_PUBLIC_ needed
const hasKey = !!apiKey
const status = await fetch('https://api.openai.com/v1/models', {
headers: { Authorization: `Bearer ${apiKey}` },
})
return (
<div>
<h1>API Status</h1>
<p>API Key Configured: {hasKey ? 'Yes' : 'No'}</p>
<p>Status: {status.ok ? 'Connected' : 'Error'}</p>
</div>
)
}Example 3: File System Access
// app/docs/[slug]/page.tsx
import fs from 'fs/promises'
import path from 'path'
import { remark } from 'remark'
import html from 'remark-html'
// ✅ Server Component - file system access
export default async function DocPage({ params }: { params: { slug: string } }) {
const filePath = path.join(process.cwd(), 'docs', `${params.slug}.md`)
const content = await fs.readFile(filePath, 'utf-8')
const processedContent = await remark()
.use(html)
.process(content)
return (
<article dangerouslySetInnerHTML={{ __html: processedContent.toString() }} />
)
}Example 4: Parallel Data Fetching
// app/dashboard/page.tsx
// ✅ Server Component - parallel fetching
export default async function DashboardPage() {
// Fetch multiple data sources in parallel
const [projects, users, stats] = await Promise.all([
db.project.findMany(),
db.user.findMany(),
db.stats.aggregate(),
])
return (
<div>
<h1>Dashboard</h1>
<StatsPanel stats={stats} />
<ProjectsGrid projects={projects} />
<UsersTable users={users} />
</div>
)
}Client Components
What Are Client Components?
Client Components run in the browser. They:
- Hydrate after initial HTML load
- Enable interactivity
- Support React hooks
- Can use browser APIs
- Must use
'use client'directive
Benefits
1. Interactivity
- Event handlers (onClick, onChange, etc.)
- Form interactions
- Real-time updates
2. React Hooks
- useState, useEffect, useContext
- Custom hooks
- React ecosystem libraries
3. Browser APIs
- localStorage, sessionStorage
- window, document
- Web APIs (Geolocation, etc.)
4. Client-Side Libraries
- Animation libraries
- Chart libraries
- Third-party UI components
Limitations
Cannot use:
asynccomponent functions- Direct database access
- File system access
- Server-only libraries
- Environment variables (without NEXT_PUBLIC_)
Can use:
- All React hooks
- Event handlers
- Browser APIs
- Client-side libraries
- useState, useEffect, etc.
When to Use Client Components
✅ USE for:
- Interactive UI elements (buttons, forms, modals)
- State management (useState, useReducer)
- Event handlers (onClick, onChange)
- Browser APIs (localStorage, window)
- Effects (useEffect, useLayoutEffect)
- React Context consumers
- Animations and transitions
- Third-party client libraries
Examples
Example 1: Interactive Form
// components/CreateProjectForm.tsx
'use client'
import { useState } from 'react'
import { createProject } from '@/app/actions'
export function CreateProjectForm() {
const [name, setName] = useState('')
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setLoading(true)
setError(null)
try {
await createProject({ name })
setName('')
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to create project')
} finally {
setLoading(false)
}
}
return (
<form onSubmit={handleSubmit}>
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Project name"
required
/>
<button type="submit" disabled={loading}>
{loading ? 'Creating...' : 'Create Project'}
</button>
{error && <p className="error">{error}</p>}
</form>
)
}Example 2: Local Storage
// components/ThemeToggle.tsx
'use client'
import { useState, useEffect } from 'react'
export function ThemeToggle() {
const [theme, setTheme] = useState<'light' | 'dark'>('light')
// Read from localStorage on mount
useEffect(() => {
const saved = localStorage.getItem('theme') as 'light' | 'dark' | null
if (saved) setTheme(saved)
}, [])
// Save to localStorage on change
const toggleTheme = () => {
const newTheme = theme === 'light' ? 'dark' : 'light'
setTheme(newTheme)
localStorage.setItem('theme', newTheme)
document.documentElement.classList.toggle('dark')
}
return (
<button onClick={toggleTheme}>
{theme === 'light' ? '🌙' : '☀️'}
</button>
)
}Example 3: Real-Time Updates
// components/LiveProjectStatus.tsx
'use client'
import { useState, useEffect } from 'react'
interface Project {
id: string
status: 'idle' | 'running' | 'completed'
}
export function LiveProjectStatus({ projectId }: { projectId: string }) {
const [project, setProject] = useState<Project | null>(null)
useEffect(() => {
// Poll for updates every 2 seconds
const interval = setInterval(async () => {
const response = await fetch(`/api/projects/${projectId}`)
const data = await response.json()
setProject(data)
}, 2000)
return () => clearInterval(interval)
}, [projectId])
if (!project) return <div>Loading...</div>
return (
<div className={`status-${project.status}`}>
Status: {project.status}
</div>
)
}Example 4: Animation
// components/AnimatedModal.tsx
'use client'
import { useState } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
export function AnimatedModal() {
const [isOpen, setIsOpen] = useState(false)
return (
<>
<button onClick={() => setIsOpen(true)}>
Open Modal
</button>
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.9 }}
className="modal"
>
<h2>Modal Content</h2>
<button onClick={() => setIsOpen(false)}>Close</button>
</motion.div>
)}
</AnimatePresence>
</>
)
}Composition Patterns
Pattern 1: Server Wrapping Client
The most common pattern - Server Component fetches data and passes to Client Component.
// app/projects/page.tsx (Server Component)
import { db } from '@/lib/db'
import { ProjectList } from '@/components/ProjectList'
// ✅ Server Component fetches data
export default async function ProjectsPage() {
const projects = await db.project.findMany()
// Pass data as props to Client Component
return (
<div>
<h1>Projects</h1>
<ProjectList projects={projects} />
</div>
)
}
// components/ProjectList.tsx (Client Component)
'use client'
import { useState } from 'react'
interface Project {
id: string
name: string
}
// ✅ Client Component handles interactivity
export function ProjectList({ projects }: { projects: Project[] }) {
const [filter, setFilter] = useState('')
const filtered = projects.filter((p) =>
p.name.toLowerCase().includes(filter.toLowerCase())
)
return (
<div>
<input
value={filter}
onChange={(e) => setFilter(e.target.value)}
placeholder="Filter projects..."
/>
<ul>
{filtered.map((project) => (
<li key={project.id}>{project.name}</li>
))}
</ul>
</div>
)
}Pattern 2: Client with Server Children
Client Component can render Server Components as children.
// components/ClientWrapper.tsx (Client Component)
'use client'
import { useState, ReactNode } from 'react'
export function ClientWrapper({ children }: { children: ReactNode }) {
const [isExpanded, setIsExpanded] = useState(false)
return (
<div>
<button onClick={() => setIsExpanded(!isExpanded)}>
{isExpanded ? 'Collapse' : 'Expand'}
</button>
{isExpanded && children}
</div>
)
}
// app/page.tsx (Server Component)
import { ClientWrapper } from '@/components/ClientWrapper'
export default async function Page() {
const data = await fetchData()
return (
<ClientWrapper>
{/* This is a Server Component rendered inside Client Component */}
<ServerDataDisplay data={data} />
</ClientWrapper>
)
}
async function ServerDataDisplay({ data }: { data: any }) {
// This is still a Server Component!
// It can do async operations, database queries, etc.
return <div>{JSON.stringify(data)}</div>
}Pattern 3: Shared Components
Some components work as both Server and Client Components.
// components/Button.tsx
// ✅ No 'use client' - works in both!
interface ButtonProps {
children: React.ReactNode
variant?: 'primary' | 'secondary'
}
export function Button({ children, variant = 'primary' }: ButtonProps) {
// Purely presentational - no hooks, no events
return (
<button className={`btn btn-${variant}`}>
{children}
</button>
)
}
// Can be used in Server Component:
export default async function ServerPage() {
return <Button>Click Me</Button>
}
// Can be used in Client Component:
'use client'
export function ClientPage() {
return <Button onClick={() => alert('Hi')}>Click Me</Button>
}Pattern 4: Context Providers
Context must be in Client Component, but can wrap Server Components.
// components/Providers.tsx (Client Component)
'use client'
import { createContext, useState, ReactNode } from 'react'
export const ThemeContext = createContext<{
theme: 'light' | 'dark'
setTheme: (theme: 'light' | 'dark') => void
}>({ theme: 'light', setTheme: () => {} })
export function Providers({ children }: { children: ReactNode }) {
const [theme, setTheme] = useState<'light' | 'dark'>('light')
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
)
}
// app/layout.tsx (Server Component)
import { Providers } from '@/components/Providers'
export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html>
<body>
<Providers>
{/* Children can be Server Components */}
{children}
</Providers>
</body>
</html>
)
}Props Passing Rules
Serialization Requirements
Props passed from Server to Client Components must be serializable.
✅ Serializable (Allowed)
// Primitives
<ClientComponent
string="hello"
number={42}
boolean={true}
null={null}
undefined={undefined}
/>
// Arrays
<ClientComponent array={[1, 2, 3]} />
// Plain objects
<ClientComponent user={{ id: 1, name: 'Alice' }} />
// Dates (converted to strings)
<ClientComponent date={new Date()} />❌ Non-Serializable (Not Allowed)
// ❌ Functions
<ClientComponent onClick={() => {}} /> // ERROR!
// ❌ Class instances
<ClientComponent user={new User()} /> // ERROR!
// ❌ Symbols
<ClientComponent sym={Symbol('id')} /> // ERROR!
// ❌ BigInt
<ClientComponent big={BigInt(9007199254740991)} /> // ERROR!
// ❌ undefined in objects (becomes null)
<ClientComponent user={{ name: undefined }} /> // Changed to null!Workarounds
Workaround 1: Server Actions
Instead of passing functions, use Server Actions.
// app/actions.ts
'use server'
export async function deleteProject(id: string) {
await db.project.delete({ where: { id } })
}
// app/page.tsx (Server Component)
import { deleteProject } from './actions'
export default async function Page() {
const projects = await db.project.findMany()
return <ProjectList projects={projects} deleteAction={deleteProject} />
}
// components/ProjectList.tsx (Client Component)
'use client'
export function ProjectList({ projects, deleteAction }: Props) {
return (
<ul>
{projects.map((p) => (
<li key={p.id}>
{p.name}
<form action={deleteAction}>
<input type="hidden" name="id" value={p.id} />
<button type="submit">Delete</button>
</form>
</li>
))}
</ul>
)
}Workaround 2: Event Handlers in Client Component
Define event handlers in Client Component, not Server Component.
// ❌ DON'T: Pass handler from Server Component
export default async function ServerPage() {
const handleClick = () => {} // Can't serialize!
return <ClientButton onClick={handleClick} /> // ERROR!
}
// ✅ DO: Define handler in Client Component
'use client'
export function ClientButton() {
const handleClick = () => {
// Handle click here
}
return <button onClick={handleClick}>Click</button>
}Common Mistakes
Mistake 1: Async Client Component
// ❌ DON'T: Async Client Component
'use client'
export default async function BadComponent() {
const data = await fetch('/api/data')
return <div>{data}</div>
}
// Error: Client Components cannot be async// ✅ DO: Use Server Component
export default async function GoodComponent() {
const data = await fetch('/api/data')
return <div>{data}</div>
}
// ✅ OR: Use useEffect in Client Component
'use client'
export default function GoodComponent() {
const [data, setData] = useState(null)
useEffect(() => {
fetch('/api/data')
.then((res) => res.json())
.then(setData)
}, [])
return <div>{data}</div>
}Mistake 2: Browser APIs in Server Component
// ❌ DON'T: Use browser APIs in Server Component
export default function BadComponent() {
const theme = localStorage.getItem('theme') // ERROR!
return <div>Theme: {theme}</div>
}// ✅ DO: Use Client Component
'use client'
export default function GoodComponent() {
const [theme, setTheme] = useState<string | null>(null)
useEffect(() => {
setTheme(localStorage.getItem('theme'))
}, [])
return <div>Theme: {theme}</div>
}Mistake 3: Hooks in Server Component
// ❌ DON'T: Use hooks in Server Component
export default async function BadComponent() {
const [count, setCount] = useState(0) // ERROR!
const data = await fetch('/api/data')
return <div>{count}</div>
}// ✅ DO: Use Client Component for hooks
'use client'
export function GoodComponent() {
const [count, setCount] = useState(0)
return <div>{count}</div>
}Mistake 4: Passing Functions as Props
// ❌ DON'T: Pass functions from Server to Client
export default async function ServerPage() {
const handleClick = () => console.log('hi') // Not serializable!
return <ClientButton onClick={handleClick} /> // ERROR!
}// ✅ DO: Use Server Actions or define in Client
'use server'
async function handleClick() {
console.log('hi')
}
export default async function ServerPage() {
return <ClientButton action={handleClick} /> // ✅ Server Action
}
// OR define in Client Component
'use client'
export function ClientButton() {
const handleClick = () => console.log('hi')
return <button onClick={handleClick}>Click</button>
}Quick Reference Checklist
When to Use Server Component
- [ ] Fetching data from database
- [ ] Fetching data from API
- [ ] Reading files
- [ ] Using Node.js APIs
- [ ] Accessing environment variables
- [ ] Static/SEO content
- [ ] No interactivity needed
When to Use Client Component
- [ ] Event handlers (onClick, etc.)
- [ ] React hooks (useState, etc.)
- [ ] Browser APIs (localStorage, etc.)
- [ ] Animations
- [ ] Form inputs with state
- [ ] Real-time updates
- [ ] Third-party client libraries
Props Checklist
- [ ] Props are serializable (JSON-compatible)
- [ ] No functions (use Server Actions)
- [ ] No class instances
- [ ] No symbols or BigInt
---
Next: Read hooks-complete.md for all React hooks reference.
Streaming Patterns - Progressive Rendering Guide
Table of Contents
- What is Streaming?
- Streaming in Next.js
- Progressive Page Loading
- Streaming Server-Rendered Content
- Streaming API Responses
- Streaming with Suspense
- Streaming Best Practices
What is Streaming?
Streaming allows you to:
- Send HTML to browser progressively
- Show content as it becomes available
- Improve perceived performance
- Keep users engaged with instant feedback
- Reduce Time to First Byte (TTFB)
Traditional Rendering vs Streaming
Traditional (All-or-Nothing):
Server: [████████████████████] (2s)
Client: [ ] ⟶ [████████████████████]
User sees: Nothing... Nothing... BOOM! Full page
Streaming (Progressive):
Server: [██ ] (0.2s) → [████ ] (0.8s) → [████████ ] (1.5s)
Client: [██ ] ⟶ [████ ] ⟶ [████████ ]
User sees: Shell → Header → Content progressivelyStreaming in Next.js
Automatic Streaming with App Router
Next.js 13+ automatically streams Server Components:
// app/page.tsx - Automatically streamed!
export default async function Page() {
const data = await fetchData()
return (
<div>
<h1>Page Title</h1>
<Content data={data} />
</div>
)
}Manual Streaming with Suspense
// app/dashboard/page.tsx
import { Suspense } from 'react'
export default function DashboardPage() {
return (
<div>
{/* Shell renders immediately */}
<DashboardHeader />
{/* Content streams as available */}
<Suspense fallback={<StatsSkeleton />}>
<StatsPanel />
</Suspense>
<Suspense fallback={<ProjectsSkeleton />}>
<ProjectsList />
</Suspense>
<Suspense fallback={<ActivitySkeleton />}>
<ActivityFeed />
</Suspense>
</div>
)
}Progressive Page Loading
Pattern 1: Shell → Content
// app/projects/page.tsx
export default function ProjectsPage() {
return (
<div>
{/* Instant shell */}
<header>
<h1>Projects</h1>
<nav>{/* Navigation */}</nav>
</header>
{/* Progressive content */}
<Suspense fallback={<PageSkeleton />}>
<ProjectsContent />
</Suspense>
</div>
)
}
async function ProjectsContent() {
const projects = await fetchProjects()
return (
<div>
{projects.map((project) => (
<ProjectCard key={project.id} project={project} />
))}
</div>
)
}Pattern 2: Above-the-Fold First
export default function Page() {
return (
<div>
{/* Above-the-fold: Render immediately */}
<Hero />
<CallToAction />
{/* Below-the-fold: Stream progressively */}
<Suspense fallback={<FeaturesSkeleton />}>
<Features />
</Suspense>
<Suspense fallback={<TestimonialsSkeleton />}>
<Testimonials />
</Suspense>
<Suspense fallback={<FooterSkeleton />}>
<Footer />
</Suspense>
</div>
)
}Pattern 3: Priority-Based Streaming
export default function Dashboard() {
return (
<div>
{/* High priority: Show first */}
<Suspense fallback={<UserSkeleton />} priority="high">
<UserInfo />
</Suspense>
{/* Medium priority: Show next */}
<Suspense fallback={<StatsSkeleton />}>
<Stats />
</Suspense>
{/* Low priority: Show last */}
<Suspense fallback={<RecommendationsSkeleton />} priority="low">
<Recommendations />
</Suspense>
</div>
)
}Streaming Server-Rendered Content
Parallel Data Fetching
// app/project/[id]/page.tsx
export default function ProjectPage({ params }: { params: { id: string } }) {
return (
<div>
{/* These all load in parallel and stream independently */}
<Suspense fallback={<ProjectHeaderSkeleton />}>
<ProjectHeader id={params.id} />
</Suspense>
<Suspense fallback={<TasksSkeleton />}>
<TasksList id={params.id} />
</Suspense>
<Suspense fallback={<CommentsSkeleton />}>
<Comments id={params.id} />
</Suspense>
<Suspense fallback={<ActivitySkeleton />}>
<Activity id={params.id} />
</Suspense>
</div>
)
}
// Each component fetches independently
async function ProjectHeader({ id }: { id: string }) {
const project = await db.project.findUnique({ where: { id } })
return <header>{project.name}</header>
}
async function TasksList({ id }: { id: string }) {
const tasks = await db.task.findMany({ where: { projectId: id } })
return <ul>{/* tasks */}</ul>
}Sequential Dependencies
export default function Page() {
return (
<div>
{/* Parent streams first */}
<Suspense fallback={<ParentSkeleton />}>
<ParentContent>
{/* Child waits for parent, then streams */}
<Suspense fallback={<ChildSkeleton />}>
<ChildContent />
</Suspense>
</ParentContent>
</Suspense>
</div>
)
}
async function ParentContent({ children }: { children: React.ReactNode }) {
const parentData = await fetchParentData()
return (
<div>
<h1>{parentData.title}</h1>
{children}
</div>
)
}
async function ChildContent() {
const childData = await fetchChildData()
return <div>{childData}</div>
}Streaming API Responses
Server-Sent Events (SSE)
// app/api/stream/route.ts
export async function GET() {
const encoder = new TextEncoder()
const stream = new ReadableStream({
async start(controller) {
// Send data progressively
for (let i = 0; i < 10; i++) {
const data = { count: i, timestamp: Date.now() }
controller.enqueue(
encoder.encode(`data: ${JSON.stringify(data)}\n\n`)
)
await new Promise((resolve) => setTimeout(resolve, 1000))
}
controller.close()
},
})
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
},
})
}// Client-side consumption
'use client'
import { useEffect, useState } from 'react'
export function StreamingData() {
const [data, setData] = useState<any[]>([])
useEffect(() => {
const eventSource = new EventSource('/api/stream')
eventSource.onmessage = (event) => {
const newData = JSON.parse(event.data)
setData((prev) => [...prev, newData])
}
return () => eventSource.close()
}, [])
return (
<ul>
{data.map((item, i) => (
<li key={i}>
Count: {item.count}, Time: {item.timestamp}
</li>
))}
</ul>
)
}Streaming JSON
// app/api/projects/stream/route.ts
import { db } from '@/lib/db'
export async function GET() {
const encoder = new TextEncoder()
const stream = new ReadableStream({
async start(controller) {
controller.enqueue(encoder.encode('['))
const projects = await db.project.findMany()
projects.forEach((project, i) => {
const data = JSON.stringify(project)
const chunk = i === 0 ? data : `,${data}`
controller.enqueue(encoder.encode(chunk))
})
controller.enqueue(encoder.encode(']'))
controller.close()
},
})
return new Response(stream, {
headers: { 'Content-Type': 'application/json' },
})
}Chunked Transfer
// app/api/large-data/route.ts
export async function GET() {
const encoder = new TextEncoder()
const stream = new ReadableStream({
async start(controller) {
const chunkSize = 1000
for (let i = 0; i < 10000; i += chunkSize) {
const chunk = await fetchDataChunk(i, chunkSize)
controller.enqueue(encoder.encode(JSON.stringify(chunk)))
// Allow browser to process
await new Promise((resolve) => setTimeout(resolve, 100))
}
controller.close()
},
})
return new Response(stream, {
headers: {
'Content-Type': 'application/json',
'Transfer-Encoding': 'chunked',
},
})
}Streaming with Suspense
Nested Boundaries
export default function Page() {
return (
<div>
<h1>Dashboard</h1>
{/* Outer boundary: Page layout */}
<Suspense fallback={<PageSkeleton />}>
<PageLayout>
{/* Inner boundary: Section content */}
<Suspense fallback={<SectionSkeleton />}>
<Section1 />
</Suspense>
<Suspense fallback={<SectionSkeleton />}>
<Section2 />
</Suspense>
</PageLayout>
</Suspense>
</div>
)
}Conditional Streaming
interface PageProps {
searchParams: { fast?: string }
}
export default function Page({ searchParams }: PageProps) {
const useFastMode = searchParams.fast === 'true'
return (
<div>
<h1>Content</h1>
{useFastMode ? (
// Fast mode: Show cached data immediately
<CachedContent />
) : (
// Normal mode: Stream fresh data
<Suspense fallback={<ContentSkeleton />}>
<FreshContent />
</Suspense>
)}
</div>
)
}Streaming with Loading States
// app/projects/loading.tsx
export default function Loading() {
return (
<div className="animate-pulse">
<div className="h-8 bg-gray-200 rounded w-1/3 mb-4"></div>
<div className="grid grid-cols-3 gap-4">
{[...Array(6)].map((_, i) => (
<div key={i} className="h-32 bg-gray-200 rounded"></div>
))}
</div>
</div>
)
}
// app/projects/page.tsx
export default async function ProjectsPage() {
const projects = await fetchProjects()
return (
<div>
<h1>Projects</h1>
<ProjectGrid projects={projects} />
</div>
)
}Streaming Best Practices
1. Strategic Suspense Boundaries
// ✅ GOOD: Boundary per major section
export default function Page() {
return (
<div>
<Suspense fallback={<HeaderSkeleton />}>
<Header />
</Suspense>
<Suspense fallback={<MainSkeleton />}>
<MainContent />
</Suspense>
<Suspense fallback={<SidebarSkeleton />}>
<Sidebar />
</Suspense>
</div>
)
}
// ❌ BAD: Too many boundaries
export default function Page() {
return (
<div>
<Suspense fallback={<Spinner />}>
<Title />
</Suspense>
<Suspense fallback={<Spinner />}>
<Subtitle />
</Suspense>
{/* Too granular! */}
</div>
)
}2. Match Skeleton to Content
// ✅ GOOD: Skeleton matches layout
function ProjectCardSkeleton() {
return (
<div className="border rounded-lg p-4">
<div className="h-6 bg-gray-200 rounded w-3/4 mb-2"></div>
<div className="h-4 bg-gray-200 rounded w-1/2 mb-4"></div>
<div className="flex gap-2">
<div className="h-8 bg-gray-200 rounded w-20"></div>
<div className="h-8 bg-gray-200 rounded w-20"></div>
</div>
</div>
)
}3. Avoid Waterfall Loading
// ❌ BAD: Sequential waterfall
async function BadComponent() {
const user = await fetchUser() // Wait 1s
const projects = await fetchProjects(user.id) // Wait 1s
const tasks = await fetchTasks(projects[0].id) // Wait 1s
// Total: 3 seconds
}
// ✅ GOOD: Parallel with independent boundaries
export default function GoodPage() {
return (
<div>
<Suspense fallback={<UserSkeleton />}>
<UserInfo />
</Suspense>
<Suspense fallback={<ProjectsSkeleton />}>
<ProjectsList />
</Suspense>
<Suspense fallback={<TasksSkeleton />}>
<TasksList />
</Suspense>
</div>
)
}
// Total: 1 second (all parallel)4. Cache Streamed Data
// Cache with Next.js fetch
async function StreamedContent() {
const data = await fetch('https://api.example.com/data', {
next: { revalidate: 3600 }, // Cache for 1 hour
})
return <div>{JSON.stringify(data)}</div>
}
export default function Page() {
return (
<Suspense fallback={<Skeleton />}>
<StreamedContent />
</Suspense>
)
}5. Progressive Enhancement
export default function Page() {
return (
<div>
{/* Critical: Render immediately */}
<CriticalContent />
{/* Important: Stream next */}
<Suspense fallback={<ImportantSkeleton />}>
<ImportantContent />
</Suspense>
{/* Nice-to-have: Stream last */}
<Suspense fallback={<OptionalSkeleton />}>
<OptionalContent />
</Suspense>
</div>
)
}6. Error Boundaries
import { ErrorBoundary } from './ErrorBoundary'
export default function Page() {
return (
<div>
<ErrorBoundary fallback={<ErrorUI />}>
<Suspense fallback={<Skeleton />}>
<AsyncContent />
</Suspense>
</ErrorBoundary>
</div>
)
}7. Metrics and Monitoring
// Track streaming performance
async function MonitoredContent() {
const start = Date.now()
const data = await fetchData()
const duration = Date.now() - start
// Log metrics
console.log(`Content loaded in ${duration}ms`)
return <div>{data}</div>
}
export default function Page() {
return (
<Suspense fallback={<Skeleton />}>
<MonitoredContent />
</Suspense>
)
}---
Next: Read migration-guide.md for React 18 → 19 migration.
Related skills
FAQ
What's new in React 19 here?
use(), useOptimistic(), useFormStatus(), useActionState(), enhanced useTransition(), improved error boundaries, and better hydration.
Can I validate my code?
Yes, the bundled validate-react.py checks Rules of Hooks and Server/Client component mistakes.