React is a JavaScript library for building user interfaces with reusable components. This cheat sheet covers the full React API — from JSX basics to advanced patterns — with copy-ready code for everyday development.
Quick reference
The 25 patterns that cover 95% of everyday React development.
| Pattern | What it does |
|---|---|
<Tag attr={value}> |
JSX element with expression |
{condition && <El />} |
Conditional render |
{ok ? <A /> : <B />} |
Ternary render |
{items.map(i => <Li key={i.id} />)} |
List render |
function Cmp({ name }) {} |
Function component with props |
const [n, setN] = useState(0) |
Local state |
setN(prev => prev + 1) |
Functional state update |
useEffect(() => {}, [dep]) |
Side effect on dep change |
useEffect(() => {}, []) |
Run once (mount) |
useEffect(() => () => cleanup(), []) |
Effect with cleanup |
const ref = useRef(null) |
DOM reference |
useMemo(() => compute(), [dep]) |
Memoize value |
useCallback(() => fn(), [dep]) |
Stable function reference |
useContext(MyCtx) |
Read context value |
const [s, dispatch] = useReducer(r, init) |
Complex state |
React.memo(Cmp) |
Skip re-render if props same |
<Suspense fallback={<Spinner />}> |
Lazy loading boundary |
const Lazy = lazy(() => import('./C')) |
Code-split component |
createPortal(el, node) |
Render outside parent DOM |
<ErrorBoundary> |
Catch render errors |
forwardRef((props, ref) => …) |
Pass ref to child |
useImperativeHandle(ref, () => ({})) |
Expose child API |
startTransition(() => setState(v)) |
Non-urgent update |
useDeferredValue(value) |
Defer expensive renders |
"use client" (Next.js) |
Mark as Client Component |
JSX syntax
JSX compiles to React.createElement calls. Every JSX file needs React in scope (or the JSX transform handles it automatically in React 17+).
// Basic element
const el = <h1 className="title">Hello</h1>;
// Self-closing
const img = <img src="/logo.png" alt="Logo" />;
// Expressions (any JS value, not statements)
const name = "World";
const greeting = <p>Hello, {name}!</p>;
// Multi-line — wrap in parentheses, one root element
const card = (
<div className="card">
<h2>{title}</h2>
<p>{body}</p>
</div>
);
// Fragment — avoid extra DOM node
const fragment = (
<>
<dt>Term</dt>
<dd>Definition</dd>
</>
);
// Attributes: camelCase, className not class, htmlFor not for
<label htmlFor="email" className="label" style={{ color: "red" }}>
Email
</label>
// Spread props
const inputProps = { type: "text", placeholder: "Name" };
<input {...inputProps} />;
// Children
<Button onClick={() => save()}>Save</Button>
Components
React components are functions that return JSX (or null).
// Function component
function Greeting({ name, age = 0 }) {
return (
<p>
Hello, {name}! You are {age} years old.
</p>
);
}
// Arrow component
const Badge = ({ label, color = "blue" }) => (
<span className={`badge badge-${color}`}>{label}</span>
);
// Component with children
function Card({ title, children }) {
return (
<div className="card">
<h2>{title}</h2>
<div className="card-body">{children}</div>
</div>
);
}
// Usage
<Card title="Welcome">
<p>Hello from inside the card.</p>
</Card>
Component naming
- PascalCase → React treats it as a component (
<MyButton />) - lowercase → HTML element (
<button />)
Props
Props flow down from parent to child. They are read-only inside the component.
// Passing props
<UserCard
name="Ana"
age={28}
active={true}
tags={["admin", "editor"]}
onLogout={() => handleLogout()}
/>
// Receiving props
function UserCard({ name, age, active, tags, onLogout }) {
return (
<div>
<h2>{name}</h2>
<p>Age: {age}</p>
{active && <span>Active</span>}
<ul>{tags.map(t => <li key={t}>{t}</li>)}</ul>
<button onClick={onLogout}>Log out</button>
</div>
);
}
// Default props (destructuring defaults)
function Button({ label = "Click me", variant = "primary", onClick }) {
return <button className={`btn-${variant}`} onClick={onClick}>{label}</button>;
}
// Spread remaining props
function Input({ label, ...rest }) {
return (
<label>
{label}
<input {...rest} />
</label>
);
}
State with useState
import { useState } from "react";
// Primitive
const [count, setCount] = useState(0);
setCount(count + 1); // direct
setCount(prev => prev + 1); // functional (safe for async/batched)
// Object state — always spread, never mutate
const [user, setUser] = useState({ name: "", email: "" });
setUser(prev => ({ ...prev, email: "new@example.com" }));
// Array state
const [items, setItems] = useState([]);
setItems(prev => [...prev, newItem]); // add
setItems(prev => prev.filter(i => i.id !== id)); // remove
setItems(prev => prev.map(i => i.id === id ? { ...i, done: true } : i)); // update
// Boolean toggle
const [open, setOpen] = useState(false);
setOpen(prev => !prev);
// Lazy initializer — runs once, avoids expensive re-computation
const [data, setData] = useState(() => JSON.parse(localStorage.getItem("data") ?? "null"));
Conditional rendering
// && short-circuit (use only when value is boolean — 0 renders as "0"!)
{isLoggedIn && <Dashboard />}
{count > 0 && <Badge count={count} />} // safe
{items.length > 0 && <List items={items} />} // length is a number — can render 0!
{items.length > 0 && <List items={items} />} // fix: explicit boolean
{!!items.length && <List items={items} />}
// Ternary
{isLoading ? <Spinner /> : <Content />}
// if/else outside JSX
function Page({ status }) {
if (status === "loading") return <Spinner />;
if (status === "error") return <Error />;
return <Content />;
}
// Nullish — render nothing
{shouldShow ? <El /> : null}
Lists and keys
// Always provide a stable, unique key
const list = items.map(item => (
<li key={item.id}>{item.name}</li>
));
// Key from index (only when list is static and never reordered)
const tabs = ["Home", "About", "Contact"].map((tab, i) => (
<button key={i}>{tab}</button>
));
// Filtered list
const active = items
.filter(item => item.active)
.map(item => <Item key={item.id} {...item} />);
// Nested lists
<ul>
{categories.map(cat => (
<li key={cat.id}>
<strong>{cat.name}</strong>
<ul>
{cat.items.map(item => (
<li key={item.id}>{item.label}</li>
))}
</ul>
</li>
))}
</ul>
Key rules: Keys must be unique among siblings. Don't use Math.random(). Avoid index keys when items can be reordered or filtered.
Events
// Click
<button onClick={() => setCount(c => c + 1)}>+</button>
// Passing arguments
<button onClick={() => deleteItem(item.id)}>Delete</button>
// Event object
<input onChange={(e) => setValue(e.target.value)} />
// Form submit — prevent default
<form onSubmit={(e) => { e.preventDefault(); submit(); }}>
// Common events
onMouseEnter / onMouseLeave
onKeyDown / onKeyUp / onKeyPress (deprecated)
onFocus / onBlur
onScroll
onDragStart / onDrop
onInput / onChange / onSubmit
// Event delegation — not needed in React (React handles it internally)
Forms
// Controlled input — React owns the value
function ContactForm() {
const [form, setForm] = useState({ name: "", email: "", message: "" });
const handle = (e) =>
setForm(prev => ({ ...prev, [e.target.name]: e.target.value }));
const submit = (e) => {
e.preventDefault();
console.log(form);
};
return (
<form onSubmit={submit}>
<input name="name" value={form.name} onChange={handle} />
<input name="email" type="email" value={form.email} onChange={handle} />
<textarea name="message" value={form.message} onChange={handle} />
<button type="submit">Send</button>
</form>
);
}
// Checkbox
const [checked, setChecked] = useState(false);
<input type="checkbox" checked={checked} onChange={e => setChecked(e.target.checked)} />
// Select
const [color, setColor] = useState("blue");
<select value={color} onChange={e => setColor(e.target.value)}>
<option value="red">Red</option>
<option value="blue">Blue</option>
</select>
// Uncontrolled input (ref)
const inputRef = useRef(null);
<input ref={inputRef} defaultValue="initial" />
// Read: inputRef.current.value
useEffect
import { useEffect } from "react";
// Run on every render
useEffect(() => { document.title = count; });
// Run once (mount)
useEffect(() => {
fetchData().then(setData);
}, []);
// Run when dep changes
useEffect(() => {
const sub = subscribe(userId);
return () => sub.unsubscribe(); // cleanup
}, [userId]);
// Data fetching with cleanup
useEffect(() => {
let cancelled = false;
async function load() {
const res = await fetch(`/api/users/${id}`);
const data = await res.json();
if (!cancelled) setUser(data);
}
load();
return () => { cancelled = true; };
}, [id]);
// Abort controller pattern
useEffect(() => {
const controller = new AbortController();
fetch(`/api/items`, { signal: controller.signal })
.then(r => r.json())
.then(setItems)
.catch(e => { if (e.name !== "AbortError") setError(e); });
return () => controller.abort();
}, []);
useRef
import { useRef } from "react";
// DOM reference
const inputRef = useRef(null);
<input ref={inputRef} />;
// inputRef.current.focus();
// Mutable value that doesn't trigger re-render
const timerRef = useRef(null);
timerRef.current = setInterval(() => tick(), 1000);
// clearInterval(timerRef.current);
// Track previous value
function usePrevious(value) {
const ref = useRef();
useEffect(() => { ref.current = value; }, [value]);
return ref.current; // previous render's value
}
Context
Use Context to share values across the tree without prop drilling.
import { createContext, useContext, useState } from "react";
// 1. Create
const ThemeContext = createContext("light");
// 2. Provide
function App() {
const [theme, setTheme] = useState("light");
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<Layout />
</ThemeContext.Provider>
);
}
// 3. Consume
function Button() {
const { theme, setTheme } = useContext(ThemeContext);
return (
<button
className={`btn-${theme}`}
onClick={() => setTheme(t => (t === "light" ? "dark" : "light"))}
>
Toggle theme
</button>
);
}
// Custom hook pattern (recommended)
function useTheme() {
const ctx = useContext(ThemeContext);
if (!ctx) throw new Error("useTheme must be used inside ThemeContext.Provider");
return ctx;
}
useReducer
Use for complex state with multiple sub-values or when next state depends on previous.
import { useReducer } from "react";
type Action =
| { type: "increment" }
| { type: "decrement" }
| { type: "reset"; payload: number };
function reducer(state: number, action: Action): number {
switch (action.type) {
case "increment": return state + 1;
case "decrement": return state - 1;
case "reset": return action.payload;
default: return state;
}
}
function Counter() {
const [count, dispatch] = useReducer(reducer, 0);
return (
<>
<p>{count}</p>
<button onClick={() => dispatch({ type: "increment" })}>+</button>
<button onClick={() => dispatch({ type: "decrement" })}>−</button>
<button onClick={() => dispatch({ type: "reset", payload: 0 })}>Reset</button>
</>
);
}
Performance
React.memo
// Skip re-render if props haven't changed (shallow comparison)
const ExpensiveList = React.memo(function ExpensiveList({ items }) {
return <ul>{items.map(i => <li key={i.id}>{i.name}</li>)}</ul>;
});
// Custom comparison
const List = React.memo(MyList, (prev, next) => prev.ids.join() === next.ids.join());
useMemo and useCallback
// Memoize expensive computation
const sorted = useMemo(() => [...items].sort(byDate), [items]);
// Stable function reference for memoized child / useEffect deps
const handleDelete = useCallback((id: string) => {
setItems(prev => prev.filter(i => i.id !== id));
}, []); // empty — setItems is stable
Code splitting
import { lazy, Suspense } from "react";
const Settings = lazy(() => import("./Settings"));
function App() {
return (
<Suspense fallback={<div>Loading…</div>}>
<Settings />
</Suspense>
);
}
Custom hooks
Extract stateful logic into reusable hooks. Name must start with use.
// useLocalStorage
function useLocalStorage<T>(key: string, initial: T) {
const [value, setValue] = useState<T>(() => {
try {
const raw = localStorage.getItem(key);
return raw !== null ? (JSON.parse(raw) as T) : initial;
} catch {
return initial;
}
});
const set = (v: T) => {
setValue(v);
localStorage.setItem(key, JSON.stringify(v));
};
return [value, set] as const;
}
// useDebounce
function useDebounce<T>(value: T, delay: number): T {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const id = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(id);
}, [value, delay]);
return debounced;
}
// useFetch
function useFetch<T>(url: string) {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
setLoading(true);
fetch(url)
.then(r => { if (!r.ok) throw new Error(r.statusText); return r.json(); })
.then(setData)
.catch(setError)
.finally(() => setLoading(false));
}, [url]);
return { data, loading, error };
}
forwardRef
Pass a ref from a parent to a DOM element inside a child component.
import { forwardRef } from "react";
const FancyInput = forwardRef<HTMLInputElement, { label: string }>(
({ label }, ref) => (
<label>
{label}
<input ref={ref} className="fancy" />
</label>
)
);
// Parent
const inputRef = useRef<HTMLInputElement>(null);
<FancyInput label="Name" ref={inputRef} />;
// inputRef.current?.focus();
Portals
Render a child outside the parent DOM hierarchy (for modals, tooltips).
import { createPortal } from "react-dom";
function Modal({ children, onClose }) {
return createPortal(
<div className="modal-overlay" onClick={onClose}>
<div className="modal" onClick={e => e.stopPropagation()}>
{children}
</div>
</div>,
document.getElementById("modal-root")!
);
}
Error boundaries
Catch JavaScript errors in the component tree. Must be class components.
class ErrorBoundary extends React.Component<
{ fallback: React.ReactNode; children: React.ReactNode },
{ error: Error | null }
> {
state = { error: null };
static getDerivedStateFromError(error: Error) {
return { error };
}
componentDidCatch(error: Error, info: React.ErrorInfo) {
console.error("ErrorBoundary caught:", error, info.componentStack);
}
render() {
if (this.state.error) return this.props.fallback;
return this.props.children;
}
}
// Usage
<ErrorBoundary fallback={<p>Something went wrong.</p>}>
<RiskyComponent />
</ErrorBoundary>
TypeScript patterns
// Component props type
interface ButtonProps {
label: string;
variant?: "primary" | "secondary";
disabled?: boolean;
onClick: () => void;
}
function Button({ label, variant = "primary", disabled = false, onClick }: ButtonProps) {
return (
<button className={`btn-${variant}`} disabled={disabled} onClick={onClick}>
{label}
</button>
);
}
// Children prop
interface WrapperProps {
children: React.ReactNode;
className?: string;
}
// Event types
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
onSubmit: (e: React.FormEvent<HTMLFormElement>) => void;
onClick: (e: React.MouseEvent<HTMLButtonElement>) => void;
onKeyDown: (e: React.KeyboardEvent<HTMLInputElement>) => void;
// Typed useState
const [user, setUser] = useState<User | null>(null);
// Typed useRef
const ref = useRef<HTMLDivElement>(null);
// Component with generic
function Select<T extends string>({
options,
value,
onChange,
}: {
options: T[];
value: T;
onChange: (v: T) => void;
}) {
return (
<select value={value} onChange={e => onChange(e.target.value as T)}>
{options.map(o => <option key={o} value={o}>{o}</option>)}
</select>
);
}
Common mistakes
| Mistake | Why it breaks | Fix |
|---|---|---|
| Mutating state directly | React doesn't detect the change | Use spread / [...arr] |
Missing key on list items |
Broken reconciliation, console warning | Use stable unique key |
Stale closure in useEffect |
Effect captures old state value | Add dep to array or use functional updater |
0 && <El /> renders 0 |
0 is falsy but renderable |
Use count > 0 && <El /> or ternary |
| Calling hooks conditionally | Breaks hook order rule | Always call hooks at top level |
useEffect with object dep |
New reference every render → infinite loop | Memoize object or use primitive deps |
Async function directly in useEffect |
Can't mark effect as async | Define async fn inside, call it immediately |
React.memo on every component |
Adds overhead for cheap renders | Only memo expensive/frequently re-rendered |
React hooks overview
| Hook | Purpose |
|---|---|
useState |
Local state |
useEffect |
Side effects (fetch, subscriptions, timers) |
useRef |
DOM reference / mutable value without re-render |
useMemo |
Memoize expensive value |
useCallback |
Stable function reference |
useContext |
Read context value |
useReducer |
Complex state machine |
useLayoutEffect |
Like useEffect but fires before paint (DOM measurements) |
useId |
Stable unique ID for accessibility |
useTransition |
Mark state update as non-urgent |
useDeferredValue |
Defer re-render for expensive child |
useImperativeHandle |
Expose imperative API via ref |
FAQ
Q: When should I use useReducer instead of useState?
When state has multiple sub-values that update together, when next state depends on current state in complex ways, or when you want to colocate state logic (the reducer) away from JSX.
Q: Why does my useEffect run twice in development?
React 18 Strict Mode runs effects twice (mount → unmount → mount) to detect side effects that aren't properly cleaned up. This only happens in development. Ensure your effect returns a cleanup function.
Q: What is the difference between useEffect and useLayoutEffect?useEffect runs asynchronously after the browser paints. useLayoutEffect runs synchronously after DOM mutations but before paint — use it for DOM measurements (element size/position) to avoid flickering.
Q: When should I reach for Context vs a state management library?
Context works well for low-frequency updates (theme, current user, locale). For high-frequency or deeply nested state (shopping cart, filters, real-time data), use a library like Zustand, Jotai, or Redux Toolkit to avoid unnecessary re-renders.
Q: Is React.memo the same as useMemo?
No. React.memo wraps a component and skips re-rendering if props haven't changed. useMemo memoizes a computed value inside a component. useCallback memoizes a function.
Q: How do I share state between sibling components?
Lift state to the closest common ancestor and pass it down as props. For deeply nested sharing, use Context. For app-wide state, use a state management library.