
React Patterns
- 2.6k installs
- 311 repo stars
- Updated June 22, 2026
- giuseppe-trisciuoglio/developer-kit
react-patterns is a React 19 skill for Server Components, Server Actions, optimistic UI, and concurrent hooks.
About
react-patterns provides React 19 guidance for Next.js App Router applications covering Server Components, Server Actions, and concurrent hooks. A quick reference maps useState, useReducer, useTransition, useDeferredValue, useOptimistic, useActionState, useFormStatus, and React Compiler auto-memoization to concrete use cases. Examples show async Server Components fetching data with client islands using useTransition, useOptimistic todo lists for instant feedback, and Server Actions validated with Zod plus useActionState form status. Instructions walk through choosing server versus client components, typing props, wrapping async trees in Suspense, optimizing with React Compiler or manual memoization, and validating public Server Action endpoints. Best practices emphasize starting server-first, adding use client only for hooks and events, keeping effects for external synchronization, and using stable list keys. React 19 specifics require Suspense around use(promise), serializable server-to-client props, and schema validation on every Server Action submission.
- Server Component and client island split with useTransition.
- useOptimistic and useActionState form mutation patterns.
- Zod-validated Server Actions with revalidatePath.
- React 19 hook quick reference including use() and useFormStatus.
- Security note treating Server Actions as public endpoints.
React Patterns by the numbers
- 2,593 all-time installs (skills.sh)
- +64 installs in the week ending Jul 28, 2026 (Skillselion tracking)
- Ranked #190 of 2,277 Frontend Development skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Jul 28, 2026 (Skillselion catalog sync)
react-patterns capabilities & compatibility
- Capabilities
- server and client component decision guidance · optimistic ui with useoptimistic and usetransiti · server action forms with zod and useactionstate · suspense and use() async render patterns · performance guidance for react compiler versus m
- Works with
- vercel
- Use cases
- frontend · ui design · testing
- IDEs
- vscode · cursor ide · webstorm
- Pricing
- Free
What react-patterns says it does
Start with Server Component (no directive needed)
addOptimisticTodo(newTodo); await addTodo(newTodo);
npx skills add https://github.com/giuseppe-trisciuoglio/developer-kit --skill react-patternsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 2.6k |
|---|---|
| repo stars | ★ 311 |
| Security audit | 3 / 3 scanners passed |
| Last updated | June 22, 2026 |
| Repository | giuseppe-trisciuoglio/developer-kit ↗ |
How do I implement React 19 App Router patterns with optimistic UI and validated Server Actions?
Implement React 19 and Next.js App Router patterns for Server Components, Server Actions, optimistic UI, and concurrent rendering.
Who is it for?
Frontend developers building Next.js App Router apps with React 19 concurrent features.
Skip if: Skip for class-component-only codebases or backend API design without React UI.
When should I use this skill?
User builds React 19 apps, Server Actions, useOptimistic, useActionState, or App Router migration.
What you get
Typed server and client components with validated actions, Suspense boundaries, and optimistic updates.
- Typed prop interfaces
- Composition examples
- State-lifting guidance
By the numbers
- ButtonProps example defines 5 typed fields including variant and size unions
Files
React 19 Development Patterns
Overview
React 19 patterns for Next.js App Router, Server Actions, optimistic UI, and concurrent features. See Quick Reference for API summary and Examples for copy-paste patterns.
When to Use
- Building React 19 applications with Next.js App Router
- Implementing optimistic UI with
useOptimisticoruseTransition - Creating Server Actions with form validation
- Migrating from class components to hooks
- Optimizing concurrent rendering with React Compiler
- Managing complex state with
useReduceror custom hooks - Wrapping async operations in Suspense boundaries
Quick Reference
| Pattern | Hook / API | Use Case |
|---|---|---|
| Local state | useState | Simple component state |
| Complex state | useReducer | Multi-action state machines |
| Side effects | useEffect | Subscriptions, data fetching |
| Shared state | useContext / createContext | Cross-component data |
| DOM access | useRef | Focus, measurements, timers |
| Performance | useMemo / useCallback | Expensive computations |
| Non-urgent updates | useTransition | Search/filter on large lists |
| Defer expensive UI | useDeferredValue | Stale-while-updating |
| Read resources | use() (React 19) | Promises and context in render |
| Optimistic UI | useOptimistic (React 19) | Instant feedback on mutations |
| Form status | useFormStatus (React 19) | Pending state in child components |
| Form state | useActionState (React 19) | Server action results |
| Auto-memoization | React Compiler | Eliminates manual memo/callback |
Instructions
1. Identify Component Type: Determine if Server Component or Client Component is needed 2. Select Hooks: Use appropriate hooks for state management and side effects 3. Type Props: Define TypeScript interfaces for all component props 4. Handle Async: Wrap data-fetching components in Suspense boundaries 5. Optimize: Use React Compiler or manual memoization for expensive renders 6. Handle Errors: Add ErrorBoundary for graceful error handling 7. Validate Server Actions: Define Zod/schema validation, then test:
- Submit invalid inputs → verify rejection
- Submit valid inputs → verify success
Examples
Server Component with Client Interaction
// Server Component (default) — async, fetches data
async function ProductPage({ id }: { id: string }) {
const product = await db.product.findUnique({ where: { id } });
return (
<div>
<h1>{product.name}</h1>
<AddToCartButton productId={product.id} />
</div>
);
}
// Client Component — handles interactivity
'use client';
function AddToCartButton({ productId }: { productId: string }) {
const [isPending, startTransition] = useTransition();
const handleAdd = () => {
startTransition(async () => {
await addToCart(productId);
});
};
return (
<button onClick={handleAdd} disabled={isPending}>
{isPending ? 'Adding...' : 'Add to Cart'}
</button>
);
}useOptimistic for Instant Feedback
'use client';
import { useOptimistic } from 'react';
function TodoList({ todos, addTodo }: { todos: Todo[]; addTodo: (t: Todo) => Promise<void> }) {
const [optimisticTodos, addOptimisticTodo] = useOptimistic(
todos,
(state, newTodo: Todo) => [...state, { ...newTodo, pending: true }]
);
const handleSubmit = async (formData: FormData) => {
const newTodo = { id: Date.now(), text: formData.get('text') as string };
addOptimisticTodo(newTodo); // Immediate UI update
await addTodo(newTodo); // Actual backend call
};
return (
<form action={handleSubmit}>
{optimisticTodos.map(todo => (
<div key={todo.id} style={{ opacity: todo.pending ? 0.5 : 1 }}>
{todo.text}
</div>
))}
<input type="text" name="text" />
<button type="submit">Add</button>
</form>
);
}Server Action with Form
// app/actions.ts
'use server';
import { z } from 'zod';
import { revalidatePath } from 'next/cache';
const schema = z.object({
title: z.string().min(5),
content: z.string().min(10),
});
export async function createPost(prevState: any, formData: FormData) {
const parsed = schema.safeParse({
title: formData.get('title'),
content: formData.get('content'),
});
if (!parsed.success) {
return { errors: parsed.error.flatten().fieldErrors };
}
await db.post.create({ data: parsed.data });
revalidatePath('/posts');
return { success: true };
}
// app/blog/new/page.tsx
'use client';
import { useActionState } from 'react';
import { createPost } from '../actions';
export default function NewPostPage() {
const [state, formAction, pending] = useActionState(createPost, {});
return (
<form action={formAction}>
<input name="title" placeholder="Title" />
{state.errors?.title && <span>{state.errors.title[0]}</span>}
<textarea name="content" placeholder="Content" />
<button type="submit" disabled={pending}>
{pending ? 'Publishing...' : 'Publish'}
</button>
</form>
);
}Custom Hook
export function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(true);
useEffect(() => {
function handleOnline() { setIsOnline(true); }
function handleOffline() { setIsOnline(false); }
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
return isOnline;
}useTransition for Non-Urgent Updates
function SearchableList({ items }: { items: Item[] }) {
const [query, setQuery] = useState('');
const [isPending, startTransition] = useTransition();
const [filteredItems, setFilteredItems] = useState(items);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setQuery(e.target.value);
startTransition(() => {
setFilteredItems(items.filter(i => i.name.toLowerCase().includes(e.target.value.toLowerCase())));
});
};
return (
<div>
<input value={query} onChange={handleChange} />
{isPending && <span>Filtering...</span>}
<ul>{filteredItems.map(i => <li key={i.id}>{i.name}</li>)}</ul>
</div>
);
}Best Practices
Server vs Client Decision
- Start with Server Component (no directive needed)
- Add
'use client'only for: hooks, browser APIs, event handlers
State Management
- Keep state minimal — compute derived values during render, not in effects
- Use
useReducerfor state with multiple related actions - Lift state up to the nearest common ancestor
Effects
- Use effects only for external system synchronization
- Always specify correct dependency arrays
- Return cleanup functions for subscriptions and timers
- Never mutate state directly — always create new references
Performance
- With React Compiler: avoid manual
useMemo,useCallback,memo - Without React Compiler: use
useMemofor expensive computations,useCallbackfor stable callbacks - Use
useTransitionfor low-priority state updates - Use stable IDs as list keys, not array indices
React 19 Specifics
- Wrap
use(promise)components in Suspense boundaries - Use
useActionStatefor form-server action integration - Validate Server Action inputs — they are public endpoints
- Pass serializable data from Server to Client Components
Constraints and Warnings
- Server Components: Cannot use hooks, event handlers, or browser APIs
- use() Hook: Can only be called during render, not in callbacks or effects
- Server Actions: Must include
'use server'directive; always validate inputs - State Mutations: Never mutate state directly — always create new references
- Effect Dependencies: Include all dependencies in
useEffectdependency arrays - Memory Leaks: Always clean up subscriptions and event listeners in useEffect return
References
Consult these files for detailed patterns:
- [references/hooks-patterns.md](references/hooks-patterns.md) — useState, useEffect, useRef, useReducer, custom hooks, common pitfalls
- [references/component-patterns.md](references/component-patterns.md) — Props, composition, lifting state, context, compound components, error boundaries
- [references/react19-features.md](references/react19-features.md) — use(), useOptimistic, useFormStatus, useActionState, Server Actions, Server Components, migration guide
- [references/performance-patterns.md](references/performance-patterns.md) — React Compiler setup, useMemo, useCallback, useTransition, useDeferredValue, lazy loading
- [references/typescript-patterns.md](references/typescript-patterns.md) — Typed props, generic components, event handlers, discriminated unions, context typing
- [references/learn.md](references/learn.md) — Progressive learning guide from basics to advanced React 19
- [references/reference.md](references/reference.md) — Complete API reference for all React hooks and component APIs
React Component Patterns
Patterns for component composition, props, state lifting, and common UI patterns.
Props and Children
Type-safe component props:
interface ButtonProps {
variant?: 'primary' | 'secondary';
size?: 'sm' | 'md' | 'lg';
onClick?: () => void;
children: React.ReactNode;
disabled?: boolean;
}
function Button({ variant = 'primary', size = 'md', onClick, children, disabled = false }: ButtonProps) {
return (
<button
className={`btn btn-${variant} btn-${size}`}
onClick={onClick}
disabled={disabled}
>
{children}
</button>
);
}Composition with children:
interface CardProps {
children: React.ReactNode;
className?: string;
}
function Card({ children, className = '' }: CardProps) {
return (
<div className={`card ${className}`}>
{children}
</div>
);
}Lifting State Up
Share state between sibling components by lifting to a common ancestor:
function Parent() {
const [activeIndex, setActiveIndex] = useState(0);
return (
<>
<Panel isActive={activeIndex === 0} onShow={() => setActiveIndex(0)}>
Panel 1 content
</Panel>
<Panel isActive={activeIndex === 1} onShow={() => setActiveIndex(1)}>
Panel 2 content
</Panel>
</>
);
}
function Panel({ isActive, onShow, children }: {
isActive: boolean;
onShow: () => void;
children: React.ReactNode;
}) {
return (
<div>
<button onClick={onShow}>Show</button>
{isActive && <div>{children}</div>}
</div>
);
}Controlled Components
Input with controlled state:
function ControlledForm() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
console.log({ name, email });
};
return (
<form onSubmit={handleSubmit}>
<input value={name} onChange={e => setName(e.target.value)} />
<input type="email" value={email} onChange={e => setEmail(e.target.value)} />
<button type="submit">Submit</button>
</form>
);
}Conditional Rendering
function Greeting({ isLoggedIn }: { isLoggedIn: boolean }) {
return (
<div>
{isLoggedIn ? <UserGreeting /> : <GuestGreeting />}
</div>
);
}
// Short-circuit for optional elements
function Notification({ message }: { message?: string }) {
return <div>{message && <p className="notification">{message}</p>}</div>;
}Lists and Keys
Always use stable IDs, not array indices:
function UserList({ users }: { users: User[] }) {
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}Compound Components Pattern
Group related components with shared context:
const MenuContext = createContext<{ isOpen: boolean; setIsOpen: (v: boolean) => void } | null>(null);
function Menu({ children }: { children: React.ReactNode }) {
const [isOpen, setIsOpen] = useState(false);
return (
<MenuContext.Provider value={{ isOpen, setIsOpen }}>
<div className="menu">{children}</div>
</MenuContext.Provider>
);
}
function MenuButton({ children }: { children: React.ReactNode }) {
const ctx = useContext(MenuContext)!;
return <button onClick={() => ctx.setIsOpen(!ctx.isOpen)}>{children}</button>;
}
function MenuItems({ children }: { children: React.ReactNode }) {
const { isOpen } = useContext(MenuContext)!;
return isOpen ? <ul>{children}</ul> : null;
}
// Usage
<Menu>
<MenuButton>Open Menu</MenuButton>
<MenuItems>
<li>Item 1</li>
<li>Item 2</li>
</MenuItems>
</Menu>Render Props Pattern
function MouseTracker({ render }: { render: (pos: { x: number; y: number }) => React.ReactNode }) {
const [position, setPosition] = useState({ x: 0, y: 0 });
return (
<div onMouseMove={e => setPosition({ x: e.clientX, y: e.clientY })}>
{render(position)}
</div>
);
}
// Usage
<MouseTracker render={({ x, y }) => <h1>Mouse: {x}, {y}</h1>} />Context for State Management
// contexts/ThemeContext.tsx
const ThemeContext = createContext<{ theme: string; setTheme: (t: string) => void } | null>(null);
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
const context = useContext(ThemeContext);
if (!context) throw new Error('useTheme must be used within ThemeProvider');
return context;
}
// Usage
function Header() {
const { theme, setTheme } = useTheme();
return (
<header className={theme}>
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Toggle
</button>
</header>
);
}Error Boundaries
class ErrorBoundary extends Component<
{ children: React.ReactNode; fallback: React.ReactNode },
{ hasError: boolean }
> {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError() {
return { hasError: true };
}
componentDidCatch(error: Error, info: React.ErrorInfo) {
console.error('Error:', error, info);
}
render() {
if (this.state.hasError) return this.props.fallback;
return this.props.children;
}
}
// Usage
<ErrorBoundary fallback={<p>Something went wrong.</p>}>
<MyComponent />
</ErrorBoundary>forwardRef Pattern
const FancyInput = forwardRef<HTMLInputElement, { placeholder?: string }>(
function FancyInput({ placeholder }, ref) {
return <input ref={ref} placeholder={placeholder} className="fancy-input" />;
}
);
// Usage
function Form() {
const inputRef = useRef<HTMLInputElement>(null);
return <FancyInput ref={inputRef} placeholder="Enter value" />;
}React Hooks Patterns
Detailed patterns for React core hooks and custom hook creation.
useState
Basic state declaration:
const [count, setCount] = useState(0);State with initializer function (expensive computation):
const [state, setState] = useState(() => {
return computeExpensiveValue();
});Multiple state variables:
function UserProfile() {
const [name, setName] = useState('');
const [age, setAge] = useState(0);
const [email, setEmail] = useState('');
return (
<form>
<input value={name} onChange={e => setName(e.target.value)} />
<input type="number" value={age} onChange={e => setAge(Number(e.target.value))} />
<input type="email" value={email} onChange={e => setEmail(e.target.value)} />
</form>
);
}useEffect
Basic effect with cleanup:
function ChatRoom({ roomId }: { roomId: string }) {
useEffect(() => {
const connection = createConnection(roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId]);
return <div>Connected to {roomId}</div>;
}Effect for subscriptions (run once):
function StatusBar() {
const [isOnline, setIsOnline] = useState(true);
useEffect(() => {
function handleOnline() { setIsOnline(true); }
function handleOffline() { setIsOnline(false); }
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []); // Empty array = run once on mount
return <h1>{isOnline ? 'Online' : 'Disconnected'}</h1>;
}Data fetching with race condition guard:
function UserProfile({ userId }: { userId: string }) {
const [user, setUser] = useState(null);
useEffect(() => {
let ignore = false;
async function fetchUser() {
const data = await api.getUser(userId);
if (!ignore) setUser(data);
}
fetchUser();
return () => { ignore = true; };
}, [userId]);
return user ? <div>{user.name}</div> : <p>Loading...</p>;
}useRef
DOM element reference:
function TextInput() {
const inputRef = useRef<HTMLInputElement>(null);
const focusInput = () => {
inputRef.current?.focus();
};
return (
<>
<input ref={inputRef} type="text" />
<button onClick={focusInput}>Focus Input</button>
</>
);
}Storing mutable values without re-renders:
function Timer() {
const intervalRef = useRef<NodeJS.Timeout | null>(null);
const startTimer = () => {
intervalRef.current = setInterval(() => console.log('Tick'), 1000);
};
const stopTimer = () => {
if (intervalRef.current) clearInterval(intervalRef.current);
};
return (
<>
<button onClick={startTimer}>Start</button>
<button onClick={stopTimer}>Stop</button>
</>
);
}useReducer
For complex state logic:
type Action =
| { type: 'increment' }
| { type: 'decrement' }
| { type: 'set'; payload: number };
function reducer(state: { count: number }, action: Action) {
switch (action.type) {
case 'increment': return { count: state.count + 1 };
case 'decrement': return { count: state.count - 1 };
case 'set': return { count: action.payload };
default: return state;
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, { count: 0 });
return (
<>
Count: {state.count}
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
</>
);
}Custom Hooks Pattern
Extract reusable logic into custom hooks:
// useOnlineStatus.ts
export function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(true);
useEffect(() => {
function handleOnline() { setIsOnline(true); }
function handleOffline() { setIsOnline(false); }
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
return isOnline;
}
// Usage
function StatusBar() {
const isOnline = useOnlineStatus();
return <h1>{isOnline ? 'Online' : 'Disconnected'}</h1>;
}Custom hook with parameters:
// useFetch.ts
interface FetchResult<T> {
data: T | null;
loading: boolean;
error: string | null;
}
export function useFetch<T>(url: string): FetchResult<T> {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let ignore = false;
async function fetchData() {
try {
setLoading(true);
const res = await fetch(url);
const result = await res.json();
if (!ignore) setData(result);
} catch (err) {
if (!ignore) setError('Failed to fetch');
} finally {
if (!ignore) setLoading(false);
}
}
fetchData();
return () => { ignore = true; };
}, [url]);
return { data, loading, error };
}Utility custom hooks:
// useDebounce.ts
export function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(handler);
}, [value, delay]);
return debouncedValue;
}
// useLocalStorage.ts
export function useLocalStorage<T>(key: string, initialValue: T) {
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch {
return initialValue;
}
});
const setValue = useCallback((value: T) => {
try {
setStoredValue(value);
window.localStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.error(error);
}
}, [key]);
return [storedValue, setValue] as const;
}Common Pitfalls
Missing dependencies:
// Wrong: missing userId in deps
useEffect(() => {
fetchData(userId);
}, []);
// Correct
useEffect(() => {
fetchData(userId);
}, [userId]);Stale closure:
// Wrong: always sees initial count
useEffect(() => {
const interval = setInterval(() => setCount(count + 1), 1000);
return () => clearInterval(interval);
}, []);
// Correct: functional update
useEffect(() => {
const interval = setInterval(() => setCount(c => c + 1), 1000);
return () => clearInterval(interval);
}, []);Avoid deriving state inside effects:
// Wrong: unnecessary effect
useEffect(() => {
setVisibleTodos(todos.filter(t => !t.completed));
}, [todos]);
// Correct: compute during render
const visibleTodos = todos.filter(t => !t.completed);React Learning Guide
Progressive learning path from React basics to advanced React 19 features.
Getting Started
Installation
Choose one of the following methods to start a new React project:
# Create React App (traditional)
npx create-react-app my-app
cd my-app
# Vite (recommended for new projects)
npm create vite@latest my-app -- --template react
cd my-app
npm install
# Next.js (full-stack React)
npx create-next-app@latest
# React Router (new v7 full-stack)
npx create-react-router@latestBasic Concepts
Your First Component
// MyComponent.js
function MyComponent() {
return <h1>Hello, React!</h1>;
}
export default MyComponent;Using Components
// App.js
import MyComponent from './MyComponent';
function App() {
return (
<div>
<MyComponent />
<p>Welcome to React</p>
</div>
);
}JSX Rules
1. Single Parent Element: Components must return one parent element
// Use fragments to avoid extra divs
return (
<>
<h1>Title</h1>
<p>Content</p>
</>
);2. Close All Tags: All tags must be closed
<img src="image.jpg" alt="description" />
<br />3. camelCase Properties: HTML attributes become camelCase
<div className="container">
<input readOnly={true} />
</div>Core Concepts
Props - Passing Data to Components
// Greeting.js
function Greeting({ name, age }) {
return (
<div>
<h1>Hello, {name}!</h1>
<p>You are {age} years old.</p>
</div>
);
}
// Usage
<Greeting name="Alice" age={30} />State - Managing Component Data
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
</div>
);
}Handling Events
function Button() {
const handleClick = () => {
alert('Button clicked!');
};
const handleMouseOver = () => {
console.log('Mouse is over button');
};
return (
<button
onClick={handleClick}
onMouseOver={handleMouseOver}
>
Click me
</button>
);
}Conditional Rendering
function Welcome({ isLoggedIn }) {
if (isLoggedIn) {
return <h1>Welcome back!</h1>;
}
return <h1>Please sign in.</h1>;
}
// Using ternary operator
function Status({ status }) {
return (
<div>
{status === 'loading' ? (
<p>Loading...</p>
) : status === 'success' ? (
<p>Success!</p>
) : (
<p>Error occurred</p>
)}
</div>
);
}
// Using logical AND
function Notification({ message }) {
return (
<div>
{message && <p className="notification">{message}</p>}
</div>
);
}Lists and Keys
function ShoppingList({ items }) {
return (
<ul>
{items.map((item, index) => (
<li key={item.id}>
{item.name} - ${item.price}
</li>
))}
</ul>
);
}
// Data
const items = [
{ id: 1, name: 'Bread', price: 2.50 },
{ id: 2, name: 'Milk', price: 3.00 },
{ id: 3, name: 'Eggs', price: 4.50 }
];Working with Forms
Controlled Components
import { useState } from 'react';
function Form() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
console.log('Submitted:', { name, email });
};
return (
<form onSubmit={handleSubmit}>
<div>
<label>
Name:
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</label>
</div>
<div>
<label>
Email:
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</label>
</div>
<button type="submit">Submit</button>
</form>
);
}React 19 Form Actions
// server/actions.js
'use server';
export async function createContact(formData) {
const name = formData.get('name');
const email = formData.get('email');
// Save to database
await db.contacts.create({ name, email });
return { success: true };
}
// components/ContactForm.js
'use client';
import { useFormState } from 'react';
import { createContact } from '../server/actions';
function ContactForm() {
const [state, formAction] = useFormState(createContact, null);
return (
<form action={formAction}>
<input name="name" placeholder="Name" required />
<input name="email" type="email" placeholder="Email" required />
<button type="submit">Add Contact</button>
{state?.success && <p>Contact added!</p>}
</form>
);
}Side Effects with useEffect
Basic Usage
import { useState, useEffect } from 'react';
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setSeconds(s => s + 1);
}, 1000);
return () => clearInterval(interval);
}, []);
return <p>Timer: {seconds}s</p>;
}Data Fetching
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
let ignore = false;
async function fetchUser() {
try {
const response = await fetch(`/api/users/${userId}`);
const userData = await response.json();
if (!ignore) {
setUser(userData);
}
} catch (error) {
console.error('Failed to fetch user:', error);
} finally {
if (!ignore) {
setLoading(false);
}
}
}
fetchUser();
return () => {
ignore = true;
};
}, [userId]);
if (loading) return <p>Loading...</p>;
if (!user) return <p>User not found</p>;
return <div>{user.name}</div>;
}Advanced Patterns
Custom Hooks
// hooks/useLocalStorage.js
import { useState } from 'react';
export function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
return initialValue;
}
});
const setValue = (value) => {
try {
setStoredValue(value);
window.localStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.error(error);
}
};
return [storedValue, setValue];
}
// Usage
import { useLocalStorage } from './hooks/useLocalStorage';
function App() {
const [name, setName] = useLocalStorage('name', '');
return (
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Enter your name"
/>
);
}Context for State Management
// contexts/ThemeContext.js
import { createContext, useContext } from 'react';
const ThemeContext = createContext(null);
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within ThemeProvider');
}
return context;
}
// Usage
function App() {
return (
<ThemeProvider>
<Header />
<Main />
</ThemeProvider>
);
}
function Header() {
const { theme, setTheme } = useTheme();
return (
<header className={theme}>
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Toggle Theme
</button>
</header>
);
}React 19 Features
use() Hook for Resources
import { Suspense, use } from 'react';
// Data fetching function
async function fetchUser(id) {
const response = await fetch(`/api/users/${id}`);
return response.json();
}
function UserProfile({ userId }) {
// Directly use the promise in component
const user = use(fetchUser(userId));
return <div>{user.name}</div>;
}
function App() {
return (
<Suspense fallback={<div>Loading profile...</div>}>
<UserProfile userId="123" />
</Suspense>
);
}useOptimistic for Optimistic UI
import { useOptimistic } from 'react';
function TodoList({ todos, addTodo }) {
const [optimisticTodos, addOptimisticTodo] = useOptimistic(
todos,
(state, newTodo) => [...state, { ...newTodo, pending: true }]
);
const handleSubmit = async (formData) => {
const text = formData.get('text');
const newTodo = { id: Date.now(), text };
// Add optimistically
addOptimisticTodo(newTodo);
// Actually add
await addTodo(newTodo);
};
return (
<div>
<form action={handleSubmit}>
<input name="text" placeholder="Add todo..." />
<button type="submit">Add</button>
</form>
<ul>
{optimisticTodos.map(todo => (
<li key={todo.id} style={{ opacity: todo.pending ? 0.5 : 1 }}>
{todo.text}
</li>
))}
</ul>
</div>
);
}useFormStatus for Form States
'use client';
import { useFormStatus } from 'react';
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? 'Submitting...' : 'Submit'}
</button>
);
}
function ContactForm() {
return (
<form action={submitContact}>
<input name="email" type="email" required />
<textarea name="message" required />
<SubmitButton />
</form>
);
}Server Components (React 19)
Server vs Client Components
// Server Component (default)
// This runs on the server
async function BlogPost({ id }) {
const post = await fetchPost(id); // Can directly await
const comments = await fetchComments(id);
return (
<article>
<h1>{post.title}</h1>
<div>{post.content}</div>
<CommentForm postId={id} />
<CommentsList comments={comments} />
</article>
);
}
// Client Component
'use client';
import { useState } from 'react';
function CommentForm({ postId }) {
const [comment, setComment] = useState('');
// Client-side interactivity
return (
<form action={addComment}>
<input
value={comment}
onChange={(e) => setComment(e.target.value)}
placeholder="Write a comment..."
/>
<button type="submit">Post</button>
</form>
);
}Performance Optimization
Code Splitting
import { lazy, Suspense } from 'react';
// Lazy load component
const LazyComponent = lazy(() => import('./LazyComponent'));
function App() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</Suspense>
</div>
);
}React.memo for Component Memoization
import { memo } from 'react';
const ExpensiveComponent = memo(function ExpensiveComponent({ data }) {
return <div>{/* Expensive rendering */}</div>;
});
// Custom comparison
const ListComponent = memo(List, (prevProps, nextProps) => {
// Only re-render if items length changed
return prevProps.items.length === nextProps.items.length;
});useMemo and useCallback
import { useMemo, useCallback } from 'react';
function Parent({ items, onItemClick }) {
const expensiveValue = useMemo(() => {
return items.reduce((sum, item) => sum + item.value, 0);
}, [items]);
const handleClick = useCallback((id) => {
onItemClick(id);
}, [onItemClick]);
return (
<div>
<p>Total: {expensiveValue}</p>
<Child items={items} onClick={handleClick} />
</div>
);
}Testing React Components
Basic Testing
import { render, screen, fireEvent } from '@testing-library/react';
import Counter from './Counter';
test('increments counter', () => {
render(<Counter />);
const button = screen.getByText('Increment');
const count = screen.getByText('Count: 0');
fireEvent.click(button);
expect(screen.getByText('Count: 1')).toBeInTheDocument();
});Testing Custom Hooks
import { renderHook, act } from '@testing-library/react';
import useCounter from './useCounter';
test('should increment counter', () => {
const { result } = renderHook(() => useCounter());
expect(result.current.count).toBe(0);
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});Best Practices
Do's
1. Use Functional Components with hooks 2. Keep Components Small and focused 3. Extract Logic into custom hooks 4. Use TypeScript for type safety 5. Write Tests for components 6. Optimize Only When Necessary - profile first
Don'ts
1. Don't Mutate State directly 2. Don't Use Index as Key for lists with additions/removals 3. Don't Create Functions in render (use useCallback) 4. Don't Ignore ESLint Rules for React hooks 5. Don't Over-optimize prematurely
Common Patterns
Compound Components
function Menu({ children }) {
const [isOpen, setIsOpen] = useState(false);
return (
<MenuContext.Provider value={{ isOpen, setIsOpen }}>
<div className="menu">{children}</div>
</MenuContext.Provider>
);
}
function MenuButton({ children }) {
const { isOpen, setIsOpen } = useContext(MenuContext);
return (
<button onClick={() => setIsOpen(!isOpen)}>
{children}
</button>
);
}
function MenuItems({ children }) {
const { isOpen } = useContext(MenuContext);
return isOpen ? <ul>{children}</ul> : null;
}
// Usage
<Menu>
<MenuButton>Open Menu</MenuButton>
<MenuItems>
<li>Item 1</li>
<li>Item 2</li>
</MenuItems>
</Menu>Render Props
function MouseTracker({ render }) {
const [position, setPosition] = useState({ x: 0, y: 0 });
const handleMouseMove = (e) => {
setPosition({ x: e.clientX, y: e.clientY });
};
return (
<div onMouseMove={handleMouseMove}>
{render(position)}
</div>
);
}
// Usage
<MouseTracker
render={({ x, y }) => (
<h1>Mouse position: {x}, {y}</h1>
)}
/>Learning Resources
Documentation
Practice Projects
1. Todo App - Basic state management 2. Weather App - API integration 3. E-commerce Site - Complex state and routing 4. Chat Application - Real-time updates 5. Blog Platform - Server Components
Common Interview Questions
1. What is the virtual DOM? 2. How does React reconcile changes? 3. What are hooks and why were they introduced? 4. Explain useEffect and its dependency array 5. What's the difference between controlled and uncontrolled components? 6. How do you optimize React performance? 7. What are Server Components in React 19?
Next Steps
1. Master the Basics: Components, props, state, events 2. Learn Hooks: useState, useEffect, useContext, custom hooks 3. Explore Ecosystem: React Router, Redux, React Query 4. Study Advanced Topics: Performance, testing, patterns 5. Stay Updated: React 19 features, Server Components, React Compiler
Remember: The best way to learn React is by building projects. Start small and gradually increase complexity as you become more comfortable with the concepts.
React Performance Patterns
Performance optimization patterns including React Compiler, memoization, and concurrent features.
React Compiler
React Compiler (available in React 19) automatically memoizes components. Write clean, idiomatic React and let the compiler optimize.
Setup
npm install -D babel-plugin-react-compiler@latest
npm install -D eslint-plugin-react-hooks@latest// babel.config.js
module.exports = {
plugins: [
'babel-plugin-react-compiler', // Must run first!
],
};
// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [
react({
babel: {
plugins: ['babel-plugin-react-compiler'],
},
}),
],
});Incremental adoption:
// babel.config.js — only compile specific directories
module.exports = {
plugins: [],
overrides: [
{
test: './src/components/**/*.{js,jsx,ts,tsx}',
plugins: ['babel-plugin-react-compiler']
}
]
};With vs Without Compiler
// Before React Compiler — manual memoization needed
const ExpensiveComponent = memo(function ExpensiveComponent({ data, onUpdate }) {
const processedData = useMemo(() => {
return data.map(item => ({ ...item, computed: expensiveCalculation(item) }));
}, [data]);
const handleClick = useCallback((id) => {
onUpdate(id);
}, [onUpdate]);
return (
<div>
{processedData.map(item => (
<Item key={item.id} item={item} onClick={handleClick} />
))}
</div>
);
});
// After React Compiler — clean idiomatic code, compiler handles it
function ExpensiveComponent({ data, onUpdate }) {
const processedData = data.map(item => ({
...item,
computed: expensiveCalculation(item)
}));
const handleClick = (id) => {
onUpdate(id);
};
return (
<div>
{processedData.map(item => (
<Item key={item.id} item={item} onClick={handleClick} />
))}
</div>
);
}Manual Memoization (Without Compiler)
React.memo
const ExpensiveComponent = memo(function ExpensiveComponent({ data }: { data: Item[] }) {
return <div>{/* expensive rendering */}</div>;
});
// Custom comparison function
const ListComponent = memo(List, (prevProps, nextProps) => {
return prevProps.items.length === nextProps.items.length;
});useMemo for Expensive Computations
function DataTable({ data }: { data: Item[] }) {
const sortedData = useMemo(() => {
return [...data].sort((a, b) => a.name.localeCompare(b.name));
}, [data]); // Only recompute when data changes
return <table>{/* render sortedData */}</table>;
}useCallback for Function Stability
function Parent({ items }: { items: Item[] }) {
const [selected, setSelected] = useState<string | null>(null);
const handleClick = useCallback((id: string) => {
setSelected(id);
}, []); // No deps needed — setSelected is stable
return items.map(item => (
<Item key={item.id} item={item} onClick={handleClick} />
));
}Concurrent Features
useTransition for Non-Urgent Updates
Mark state updates as non-urgent to keep the UI responsive:
function SearchableList({ items }: { items: Item[] }) {
const [query, setQuery] = useState('');
const [isPending, startTransition] = useTransition();
const [filteredItems, setFilteredItems] = useState(items);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setQuery(e.target.value); // Immediate update
startTransition(() => {
setFilteredItems(
items.filter(item =>
item.name.toLowerCase().includes(e.target.value.toLowerCase())
)
);
});
};
return (
<div>
<input value={query} onChange={handleChange} placeholder="Search..." />
{isPending && <div>Filtering...</div>}
<ul>
{filteredItems.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</div>
);
}useDeferredValue for Expensive UI
Defer an expensive computation to keep input responsive:
function DataGrid({ data }: { data: DataRow[] }) {
const [searchTerm, setSearchTerm] = useState('');
const deferredSearchTerm = useDeferredValue(searchTerm);
const filteredData = useMemo(() => {
return data.filter(row =>
Object.values(row).some(value =>
String(value).toLowerCase().includes(deferredSearchTerm.toLowerCase())
)
);
}, [data, deferredSearchTerm]);
return (
<div>
<input
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
className={searchTerm !== deferredSearchTerm ? 'stale' : ''}
/>
<DataGridRows data={filteredData} />
</div>
);
}useTransition vs useDeferredValue
| useTransition | useDeferredValue | |
|---|---|---|
| Use when | You control the state update | You receive value as prop |
| Shows pending | Yes (isPending) | No (visual stale indicator) |
| Pattern | Wrap setState calls | Wrap the value |
Lazy Loading
import { lazy, Suspense } from 'react';
const LazyDashboard = lazy(() => import('./Dashboard'));
const LazySettings = lazy(() => import('./Settings'));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<LazyDashboard />
</Suspense>
);
}Browser Observer Hooks
useResizeObserver
function useResizeObserver(elementRef: React.RefObject<HTMLElement>) {
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
useEffect(() => {
const element = elementRef.current;
if (!element) return;
const observer = new ResizeObserver(entries => {
const entry = entries[0];
setDimensions({
width: entry.contentRect.width,
height: entry.contentRect.height
});
});
observer.observe(element);
return () => observer.disconnect();
}, [elementRef]);
return dimensions;
}useIntersectionObserver
function useIntersectionObserver(
elementRef: React.RefObject<HTMLElement>,
options?: IntersectionObserverInit
) {
const [isIntersecting, setIsIntersecting] = useState(false);
useEffect(() => {
const element = elementRef.current;
if (!element) return;
const observer = new IntersectionObserver(
([entry]) => setIsIntersecting(entry.isIntersecting),
options
);
observer.observe(element);
return () => observer.disconnect();
}, [elementRef, options]);
return isIntersecting;
}Avoid Unnecessary Effects
Compute derived state during render, not in effects:
// Wrong: effect for derived state
function TodoList({ todos }: { todos: Todo[] }) {
const [visibleTodos, setVisibleTodos] = useState<Todo[]>([]);
useEffect(() => {
setVisibleTodos(todos.filter(t => !t.completed));
}, [todos]);
}
// Correct: compute during render
function TodoList({ todos }: { todos: Todo[] }) {
const visibleTodos = todos.filter(t => !t.completed);
return <ul>{/* render visibleTodos */}</ul>;
}Performance Checklist
- Profile with React DevTools before optimizing
- Avoid premature optimization — measure first
- With React Compiler: remove manual
useMemo,useCallback,memo - Without React Compiler: wrap expensive computations with
useMemo - Use
useTransitionfor filtering/sorting large lists - Use
useDeferredValuewhen value comes from props or external state - Use
lazy()andSuspensefor code splitting large components - Keep component render functions pure — no side effects
- Use stable keys for lists (avoid index when items can be added/removed)
React 19 Features
Patterns for React 19's new hooks, Server Components, Server Actions, and form handling.
use() Hook
Read a Promise or Context value during render:
import { use } from 'react';
// Reading a Promise
function MessageComponent({ messagePromise }: { messagePromise: Promise<string> }) {
const message = use(messagePromise);
return <p>{message}</p>;
}
// Reading Context conditionally (unlike useContext, can be called in conditionals)
function Button({ condition }: { condition: boolean }) {
if (condition) {
const theme = use(ThemeContext);
return <button className={theme}>Click</button>;
}
return <button>Click</button>;
}Must be wrapped in Suspense when reading promises:
function App() {
const messagePromise = fetchMessage();
return (
<Suspense fallback={<p>Loading...</p>}>
<MessageComponent messagePromise={messagePromise} />
</Suspense>
);
}useOptimistic
Optimistic UI updates for async operations:
import { useOptimistic } from 'react';
function TodoList({ todos, addTodo }: { todos: Todo[]; addTodo: (t: Todo) => Promise<void> }) {
const [optimisticTodos, addOptimisticTodo] = useOptimistic(
todos,
(state, newTodo: Todo) => [...state, { ...newTodo, pending: true }]
);
const handleSubmit = async (formData: FormData) => {
const newTodo = { id: Date.now(), text: formData.get('text') as string };
addOptimisticTodo(newTodo); // Immediate UI update
await addTodo(newTodo); // Actual backend call
};
return (
<form action={handleSubmit}>
{optimisticTodos.map(todo => (
<div key={todo.id} style={{ opacity: todo.pending ? 0.5 : 1 }}>
{todo.text}
</div>
))}
<input type="text" name="text" />
<button type="submit">Add</button>
</form>
);
}useFormStatus
Access form submission status in child components:
import { useFormStatus } from 'react';
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? 'Submitting...' : 'Submit'}
</button>
);
}
function ContactForm() {
return (
<form action={submitForm}>
<input name="email" type="email" />
<SubmitButton /> {/* Must be inside the <form> */}
</form>
);
}useFormState / useActionState
Manage form state with server action results:
import { useFormState } from 'react'; // React 19: useActionState
async function submitAction(prevState: string | null, formData: FormData) {
const email = formData.get('email') as string;
if (!email.includes('@')) {
return 'Invalid email address';
}
await submitToDatabase(email);
return null;
}
function EmailForm() {
const [state, formAction] = useFormState(submitAction, null);
return (
<form action={formAction}>
<input name="email" type="email" />
<button type="submit">Subscribe</button>
{state && <p className="error">{state}</p>}
</form>
);
}Server Actions
Define server-side functions for mutations and form handling:
// app/actions.ts
'use server';
import { redirect } from 'next/navigation';
import { revalidatePath } from 'next/cache';
export async function createPost(formData: FormData) {
const title = formData.get('title') as string;
const content = formData.get('content') as string;
if (!title || !content) {
return { error: 'Title and content are required' };
}
const post = await db.post.create({ data: { title, content } });
revalidatePath('/posts');
redirect(`/posts/${post.id}`);
}Server Action with Zod validation:
'use server';
import { z } from 'zod';
const checkoutSchema = z.object({
items: z.array(z.object({
productId: z.string(),
quantity: z.number().min(1)
})),
shippingAddress: z.object({
street: z.string().min(1),
city: z.string().min(1),
zipCode: z.string().regex(/^\d{5}$/)
}),
paymentMethod: z.enum(['credit', 'paypal', 'apple'])
});
export async function processCheckout(prevState: any, formData: FormData) {
const rawData = {
items: JSON.parse(formData.get('items') as string),
shippingAddress: {
street: formData.get('street'),
city: formData.get('city'),
zipCode: formData.get('zipCode')
},
paymentMethod: formData.get('paymentMethod')
};
const result = checkoutSchema.safeParse(rawData);
if (!result.success) {
return {
error: 'Validation failed',
fieldErrors: result.error.flatten().fieldErrors
};
}
try {
const order = await createOrder(result.data);
await updateInventory(result.data.items);
await sendConfirmationEmail(order);
revalidatePath('/orders');
return { success: true, orderId: order.id };
} catch {
return { error: 'Payment failed' };
}
}Server Components
Components that run exclusively on the server:
// app/posts/page.tsx — Server Component (default)
async function PostsPage() {
const posts = await db.post.findMany({
orderBy: { createdAt: 'desc' },
take: 10
});
return (
<div>
<h1>Latest Posts</h1>
<PostsList posts={posts} />
</div>
);
}Mixed Server/Client architecture:
// Server Component — handles data fetching
async function ProductPage({ id }: { id: string }) {
const product = await fetchProduct(id);
const related = await fetchRelatedProducts(id);
return (
<div>
<ProductDetails product={product} />
<RelatedProducts products={related} />
</div>
);
}
// Client Component — handles interactivity
'use client';
function ProductDetails({ product }: { product: Product }) {
const [quantity, setQuantity] = useState(1);
const [isAdded, setIsAdded] = useState(false);
return (
<div>
<h1>{product.name}</h1>
<p>${product.price}</p>
<input type="number" value={quantity} onChange={e => setQuantity(Number(e.target.value))} min="1" />
<AddToCartButton productId={product.id} quantity={quantity} onAdded={() => setIsAdded(true)} />
{isAdded && <p>Added to cart!</p>}
</div>
);
}Migration from React 18 to 19
1. Update dependencies:
npm install react@19 react-dom@192. Replace manual optimistic updates with useOptimistic:
// Before (React 18)
function TodoList({ todos, addTodo }) {
const [optimisticTodos, setOptimisticTodos] = useState(todos);
const handleAdd = async (text) => {
const newTodo = { id: Date.now(), text };
setOptimisticTodos([...optimisticTodos, newTodo]);
await addTodo(newTodo);
};
}
// After (React 19)
function TodoList({ todos, addTodo }) {
const [optimisticTodos, addOptimisticTodo] = useOptimistic(
todos,
(state, newTodo) => [...state, newTodo]
);
const handleAdd = async (formData) => {
const newTodo = { id: Date.now(), text: formData.get('text') };
addOptimisticTodo(newTodo);
await addTodo(newTodo);
};
}3. Enable React Compiler and remove manual memoization (see performance-patterns.md)
Common Pitfalls
// Wrong: use() outside render
function handleClick() {
const data = use(promise); // Error: can only be called in render
}
// Correct
function Component({ promise }) {
const data = use(promise); // Called during render
return <div>{data}</div>;
}
// Wrong: missing 'use server'
export async function myAction() {
// This runs on the client!
}
// Correct
'use server';
export async function myAction() {
// Runs on the server
}
// Wrong: browser APIs in Server Component
export default async function ServerComponent() {
const width = window.innerWidth; // Error: window is not defined
return <div>{width}</div>;
}
// Correct: delegate to Client Component
export default async function ServerComponent() {
const data = await fetchData();
return <ClientComponent data={data} />;
}React Reference Guide
Complete reference for React hooks, APIs, and patterns including React 19 features.
Core Hooks Reference
useState
const [state, setState] = useState(initialState);
const [state, setState] = useState(() => computeInitialState());Parameters:
initialState: Initial state value or function- Returns:
[state, setState]tuple
Examples:
// Basic counter
const [count, setCount] = useState(0);
// With function initializer
const [data, setData] = useState(() => {
return JSON.parse(localStorage.getItem('data') || '[]');
});
// Object state
const [user, setUser] = useState({
name: '',
email: '',
age: 0
});useEffect
useEffect(() => {
// Side effect logic
return () => {
// Cleanup
};
}, [dependencies]);Parameters:
- Setup function (can return cleanup function)
- Dependency array (optional)
Common Patterns:
// Data fetching
useEffect(() => {
let ignore = false;
async function fetchData() {
const result = await api.getData();
if (!ignore) {
setData(result);
}
}
fetchData();
return () => {
ignore = true;
};
}, []);
// Subscriptions
useEffect(() => {
const subscription = eventSource.subscribe(handleEvent);
return () => subscription.unsubscribe();
}, [handleEvent]);
// DOM manipulation
useEffect(() => {
const element = ref.current;
if (element) {
element.focus();
}
}, []);useContext
const value = useContext(MyContext);Usage:
// Create context
const ThemeContext = createContext('light');
// Provider
function App() {
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
// Consumer
function Button() {
const theme = useContext(ThemeContext);
return <button className={theme}>Click me</button>;
}useReducer
const [state, dispatch] = useReducer(reducer, initialState);
const [state, dispatch] = useReducer(reducer, initialState, init);Example:
interface State {
count: number;
}
type Action =
| { type: 'increment' }
| { type: 'decrement' }
| { type: 'set'; payload: number };
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 'set':
return { count: action.payload };
default:
return state;
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, { count: 0 });
return (
<>
Count: {state.count}
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
</>
);
}useRef
const ref = useRef(initialValue);Use Cases:
// DOM reference
const inputRef = useRef<HTMLInputElement>(null);
// Mutable value
const timerRef = useRef<NodeJS.Timeout | null>(null);
// Previous value
const prevValueRef = useRef();
useEffect(() => {
prevValueRef.current = value;
});useMemo
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);useCallback
const memoizedCallback = useCallback(
() => doSomething(a, b),
[a, b]
);useLayoutEffect
useLayoutEffect(() => {
// Runs synchronously after DOM mutations
});React 19 Hooks
use
const value = use(resource);Reading Promises:
function Message({ messagePromise }) {
const message = use(messagePromise);
return <p>{message}</p>;
}Reading Context:
function ThemeButton() {
if (condition) {
const theme = use(ThemeContext);
return <button className={theme}>Click</button>;
}
return <button>Click</button>;
}useOptimistic
const [optimisticState, addOptimistic] = useOptimistic(
state,
updateFn
);useFormStatus
const { pending, data, method, action } = useFormStatus();useFormState
const [state, formAction] = useFormState(
actionFn,
initialState,
permalink?
);Advanced Hooks
useImperativeHandle
useImperativeHandle(ref, createHandle, [deps]);useId
const id = useId();useDebugValue
useDebugValue(value, formatFn);useDeferredValue
const deferredValue = useDeferredValue(value);useTransition
const [isPending, startTransition] = useTransition();Component APIs
memo
const MemoizedComponent = memo(Component, areEqual?);forwardRef
const RefComponent = forwardRef((props, ref) => {
return <div ref={ref}>{props.children}</div>;
});lazy
const LazyComponent = lazy(() => import('./Component'));Server Components APIs
Server Actions
'use server';
async function myAction(formData: FormData) {
// Server-side logic
}Server Component Directives
// Default: Server Component
export default async function ServerComponent() {
const data = await fetch('...');
return <div>{data}</div>;
}
// Client Component
'use client';
export function ClientComponent() {
const [state, setState] = useState();
return <div>{state}</div>;
}Common Patterns
Custom Hook Template
function useCustomHook(initialValue) {
const [state, setState] = useState(initialValue);
// Custom logic
const updateValue = useCallback((newValue) => {
setState(newValue);
}, []);
return [state, updateValue];
}Data Fetching Hook
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let ignore = false;
async function fetchData() {
try {
setLoading(true);
const response = await fetch(url);
const result = await response.json();
if (!ignore) {
setData(result);
}
} catch (err) {
if (!ignore) {
setError(err);
}
} finally {
if (!ignore) {
setLoading(false);
}
}
}
fetchData();
return () => {
ignore = true;
};
}, [url]);
return { data, loading, error };
}Local Storage Hook
function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
return initialValue;
}
});
const setValue = useCallback((value) => {
try {
setStoredValue(value);
window.localStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.error(error);
}
}, [key]);
return [storedValue, setValue];
}Debounce Hook
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
}Performance Optimization
Component Memoization
// With memo
const ExpensiveComponent = memo(function ExpensiveComponent({ data }) {
return <div>{processData(data)}</div>;
});
// Custom comparison
const ListComponent = memo(List, (prevProps, nextProps) => {
return prevProps.items.length === nextProps.items.length;
});Callback Optimization
function Parent({ items }) {
const [selected, setSelected] = useState(null);
const handleClick = useCallback((id) => {
setSelected(id);
}, []);
return items.map(item => (
<Item key={item.id} item={item} onClick={handleClick} />
));
}Memo Optimization
function ExpensiveCalculation({ data, filter }) {
const filteredData = useMemo(() => {
return data.filter(item => item.type === filter);
}, [data, filter]);
const summary = useMemo(() => {
return filteredData.reduce((acc, item) => acc + item.value, 0);
}, [filteredData]);
return <div>Total: {summary}</div>;
}Error Handling
Error Boundaries
class ErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
console.error('Error:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}Error Handling with Hooks
function useErrorHandler() {
const [error, setError] = useState(null);
const resetError = useCallback(() => {
setError(null);
}, []);
const handleError = useCallback((error) => {
setError(error);
}, []);
return { error, handleError, resetError };
}TypeScript Integration
Component Props Types
interface ButtonProps {
variant: 'primary' | 'secondary';
size?: 'sm' | 'md' | 'lg';
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
children: React.ReactNode;
disabled?: boolean;
}
const Button: React.FC<ButtonProps> = ({
variant,
size = 'md',
onClick,
children,
disabled = false
}) => {
return (
<button
className={`btn btn-${variant} btn-${size}`}
onClick={onClick}
disabled={disabled}
>
{children}
</button>
);
};Generic Components
interface ListProps<T> {
items: T[];
renderItem: (item: T, index: number) => React.ReactNode;
keyExtractor: (item: T) => string | number;
}
function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
return (
<ul>
{items.map((item, index) => (
<li key={keyExtractor(item)}>
{renderItem(item, index)}
</li>
))}
</ul>
);
}Hook Return Types
interface UseApiResult<T> {
data: T | null;
loading: boolean;
error: string | null;
refetch: () => void;
}
function useApi<T>(url: string): UseApiResult<T> {
// Implementation
}Testing Utilities
Render with Providers
function renderWithProviders(
ui: React.ReactElement,
options: RenderOptions = {}
) {
function Wrapper({ children }: { children: React.ReactNode }) {
return (
<Provider store={store}>
<Router>
{children}
</Router>
</Provider>
);
}
return render(ui, { wrapper: Wrapper, ...options });
}Testing Custom Hooks
import { renderHook, act } from '@testing-library/react';
test('useCounter increments', () => {
const { result } = renderHook(() => useCounter());
expect(result.current.count).toBe(0);
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});Common Gotchas
Dependency Array Rules
// ❌ Missing dependencies
useEffect(() => {
fetchData(userId);
}, []); // Missing userId
// ✅ Correct
useEffect(() => {
fetchData(userId);
}, [userId]);Stale Closure
// ❌ Stale closure
useEffect(() => {
const interval = setInterval(() => {
setCount(count + 1); // Always sees initial count
}, 1000);
return () => clearInterval(interval);
}, []);
// ✅ Functional update
useEffect(() => {
const interval = setInterval(() => {
setCount(c => c + 1);
}, 1000);
return () => clearInterval(interval);
}, []);Infinite Re-renders
// ❌ Infinite loop
useEffect(() => {
setData(processData(data));
}, [data]);
// ✅ Correct approach
useEffect(() => {
setData(processData(data));
}, [someOtherDependency]);Browser APIs Integration
Resize Observer
function useResizeObserver(elementRef) {
const [dimensions, setDimensions] = useState({
width: 0,
height: 0
});
useEffect(() => {
const element = elementRef.current;
if (!element) return;
const observer = new ResizeObserver(entries => {
const entry = entries[0];
setDimensions({
width: entry.contentRect.width,
height: entry.contentRect.height
});
});
observer.observe(element);
return () => observer.disconnect();
}, [elementRef]);
return dimensions;
}Intersection Observer
function useIntersectionObserver(elementRef, options) {
const [isIntersecting, setIsIntersecting] = useState(false);
useEffect(() => {
const element = elementRef.current;
if (!element) return;
const observer = new IntersectionObserver(
([entry]) => setIsIntersecting(entry.isIntersecting),
options
);
observer.observe(element);
return () => observer.disconnect();
}, [elementRef, options]);
return isIntersecting;
}Migration Checklist
React 18 to 19 Migration
- [ ] Update dependencies to React 19
- [ ] Replace manual optimistic updates with
useOptimistic - [ ] Migrate form handling to use Server Actions
- [ ] Add
'use client'directive to client components - [ ] Enable React Compiler for performance
- [ ] Update ESLint configuration for new hooks
- [ ] Test concurrent features properly
- [ ] Verify hydration behavior with Suspense
Class Components to Hooks Migration
- [ ] Convert
this.statetouseState - [ ] Replace
componentDidMountwithuseEffect - [ ] Convert
componentDidUpdatetouseEffectwith dependencies - [ ] Replace
componentWillUnmountwith cleanup inuseEffect - [ ] Convert methods to useCallback if passed as props
- [ ] Replace context consumers with
useContext - [ ] Convert refs using
useRef - [ ] Replace HOCs with custom hooks where appropriate
TypeScript Patterns for React
TypeScript integration patterns for React components, hooks, and events.
Component Props
Basic interface:
interface UserProps {
id: string;
name: string;
email: string;
age?: number; // Optional
}
function User({ id, name, email, age }: UserProps) {
return (
<div>
<h2>{name}</h2>
<p>{email}</p>
{age && <p>Age: {age}</p>}
</div>
);
}Using React.FC (explicit):
interface ButtonProps {
variant: 'primary' | 'secondary';
size?: 'sm' | 'md' | 'lg';
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
children: React.ReactNode;
disabled?: boolean;
}
const Button: React.FC<ButtonProps> = ({
variant,
size = 'md',
onClick,
children,
disabled = false
}) => (
<button
className={`btn btn-${variant} btn-${size}`}
onClick={onClick}
disabled={disabled}
>
{children}
</button>
);Generic Components
Type-safe list component:
interface ListProps<T> {
items: T[];
renderItem: (item: T, index: number) => React.ReactNode;
keyExtractor: (item: T) => string | number;
}
function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
return (
<ul>
{items.map((item, index) => (
<li key={keyExtractor(item)}>{renderItem(item, index)}</li>
))}
</ul>
);
}
// Usage
<List
items={users}
renderItem={(user) => <span>{user.name}</span>}
keyExtractor={(user) => user.id}
/>Event Handlers
Typed event handlers:
function Form() {
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
console.log(e.target.value);
};
const handleSelect = (e: React.ChangeEvent<HTMLSelectElement>) => {
console.log(e.target.value);
};
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
console.log('Clicked');
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') console.log('Enter pressed');
};
return (
<form onSubmit={handleSubmit}>
<input onChange={handleChange} onKeyDown={handleKeyDown} />
<select onChange={handleSelect}>
<option value="a">A</option>
</select>
<button onClick={handleClick}>Submit</button>
</form>
);
}Hook Return Types
Typed custom hook:
interface UseApiResult<T> {
data: T | null;
loading: boolean;
error: string | null;
refetch: () => void;
}
function useApi<T>(url: string): UseApiResult<T> {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const refetch = useCallback(() => {
// Trigger re-fetch
}, [url]);
return { data, loading, error, refetch };
}
// Usage
const { data, loading } = useApi<User[]>('/api/users');Discriminated Unions for State
type AsyncState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: string };
function useAsyncData<T>(url: string): AsyncState<T> {
const [state, setState] = useState<AsyncState<T>>({ status: 'idle' });
useEffect(() => {
setState({ status: 'loading' });
fetch(url)
.then(res => res.json())
.then(data => setState({ status: 'success', data }))
.catch(err => setState({ status: 'error', error: err.message }));
}, [url]);
return state;
}
// Usage
function UserPage() {
const state = useAsyncData<User>('/api/user/1');
if (state.status === 'loading') return <p>Loading...</p>;
if (state.status === 'error') return <p>Error: {state.error}</p>;
if (state.status === 'success') return <div>{state.data.name}</div>;
return null;
}Context Typing
interface AuthContextType {
user: User | null;
login: (credentials: Credentials) => Promise<void>;
logout: () => void;
}
const AuthContext = createContext<AuthContextType | null>(null);
export function useAuth(): AuthContextType {
const ctx = useContext(AuthContext);
if (!ctx) throw new Error('useAuth must be used within AuthProvider');
return ctx;
}
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const login = async (credentials: Credentials) => {
const user = await authApi.login(credentials);
setUser(user);
};
const logout = () => setUser(null);
return (
<AuthContext.Provider value={{ user, login, logout }}>
{children}
</AuthContext.Provider>
);
}Ref Typing
// DOM element refs
const inputRef = useRef<HTMLInputElement>(null);
const divRef = useRef<HTMLDivElement>(null);
const buttonRef = useRef<HTMLButtonElement>(null);
// Mutable value (not null)
const counterRef = useRef<number>(0);
const timerRef = useRef<NodeJS.Timeout | null>(null);
// Previous value pattern
function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T>();
useEffect(() => {
ref.current = value;
});
return ref.current;
}Component Prop Patterns
Extending HTML element props:
interface CustomButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary';
loading?: boolean;
}
function CustomButton({ variant = 'primary', loading, children, ...rest }: CustomButtonProps) {
return (
<button className={`btn btn-${variant}`} disabled={loading} {...rest}>
{loading ? 'Loading...' : children}
</button>
);
}Polymorphic component (render as different element):
interface BoxProps<T extends React.ElementType> {
as?: T;
children: React.ReactNode;
}
function Box<T extends React.ElementType = 'div'>({
as,
children,
...props
}: BoxProps<T> & Omit<React.ComponentPropsWithoutRef<T>, keyof BoxProps<T>>) {
const Component = as || 'div';
return <Component {...props}>{children}</Component>;
}
// Usage
<Box as="section" className="container">Content</Box>
<Box as="article">Article content</Box>Testing Utilities
Typed render helper with providers:
import { render, RenderOptions } from '@testing-library/react';
function renderWithProviders(
ui: React.ReactElement,
options: RenderOptions = {}
) {
function Wrapper({ children }: { children: React.ReactNode }) {
return (
<AuthProvider>
<ThemeProvider>
{children}
</ThemeProvider>
</AuthProvider>
);
}
return render(ui, { wrapper: Wrapper, ...options });
}Related skills
Forks & variants (1)
React Patterns has 1 known copy in the catalog totaling 31 installs. They canonicalize to this original listing.
- giuseppe-trisciuoglio - 31 installs
How it compares
Pick react-patterns over generic frontend skills when you need concrete React composition and prop-typing recipes rather than framework-agnostic UI advice.
FAQ
When add the use client directive?
Only when you need hooks, browser APIs, or event handlers in that file.
Must Server Action inputs be validated?
Yes. Server Actions are public endpoints and need schema validation on every submission.
Should I manually memoize with React Compiler?
With React Compiler enabled, avoid manual useMemo, useCallback, and memo in most cases.
Is React Patterns safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.