React Reference
React Reference
React 18/19 patterns you use every day: component patterns, state, performance, data fetching, Server Components, and the hooks that replace most class boilerplate.
Component patterns — function, memo, forwardRef
// Function component — the standard
function Button({ label, onClick, disabled = false, variant = "primary" }) {
return (
);
}
// TypeScript props
interface ButtonProps {
label: string;
onClick: () => void;
disabled?: boolean;
variant?: "primary" | "secondary" | "danger";
children?: React.ReactNode;
}
// React.memo — skip re-render when props haven't changed
const ExpensiveList = React.memo(function ExpensiveList({ items }) {
return {items.map(i => - {i.name}
)}
;
});
// Custom comparison function (when shallow equality isn't enough)
const Chart = React.memo(Chart, (prev, next) =>
prev.data === next.data && prev.height === next.height
);
// forwardRef — expose DOM node to parent
const Input = React.forwardRef(function Input({ label, ...props }, ref) {
return (
);
});
// useImperativeHandle — expose custom methods (not just DOM node)
const Dialog = React.forwardRef(function Dialog({ children }, ref) {
const dialogRef = useRef();
useImperativeHandle(ref, () => ({
open: () => dialogRef.current.showModal(),
close: () => dialogRef.current.close(),
}));
return ;
});
// Compound components pattern
const Tabs = Object.assign(TabsRoot, { Tab, Panel, List });
State patterns — useState, useReducer, lifting state
// useState — simple values
const [count, setCount] = useState(0);
const [user, setUser] = useState(null);
// Functional update — always use when new state depends on old
setCount(c => c + 1); // safe even in StrictMode / batching
setItems(prev => [...prev, item]);
// useState with lazy initialisation (expensive initial value)
const [state] = useState(() => JSON.parse(localStorage.getItem("state") || "{}"));
// useReducer — for complex state transitions
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;
}
}
const [count, dispatch] = useReducer(reducer, 0);
dispatch({ type: "reset", payload: 10 });
// Lifting state — move shared state to closest common ancestor
// BAD: two siblings sharing state via props drilling
// GOOD: lift state to parent, pass down via props or Context
// Object state — spread to update one field
const [form, setForm] = useState({ name: "", email: "", role: "user" });
const updateField = (field: string) => (e: React.ChangeEvent) =>
setForm(prev => ({ ...prev, [field]: e.target.value }));
// Batching (React 18+) — multiple setStates in event handlers
// and async code are batched automatically
async function handleClick() {
setLoading(true);
const data = await fetch("/api"); // batched with above
setData(data);
setLoading(false);
}
useEffect — data fetching, subscriptions, cleanup
// Basic effect
useEffect(() => {
document.title = `${count} items`;
}, [count]); // only re-run when count changes
// Empty array = run once after mount
useEffect(() => {
const id = analytics.init();
return () => analytics.cleanup(id); // cleanup on unmount
}, []);
// No array = run after EVERY render (almost never what you want)
// Data fetching in useEffect (old pattern — prefer React Query or use)
useEffect(() => {
let cancelled = false; // handle stale closures
async function load() {
setLoading(true);
try {
const data = await fetchUser(userId);
if (!cancelled) setUser(data);
} catch (err) {
if (!cancelled) setError(err);
} finally {
if (!cancelled) setLoading(false);
}
}
load();
return () => { cancelled = true; };
}, [userId]);
// AbortController — cancel fetch on cleanup
useEffect(() => {
const controller = new AbortController();
fetch(`/api/users/${id}`, { signal: controller.signal })
.then(r => r.json())
.then(setUser)
.catch(err => { if (err.name !== "AbortError") setError(err); });
return () => controller.abort();
}, [id]);
// Subscription pattern
useEffect(() => {
const unsub = store.subscribe(() => setState(store.getState()));
return unsub;
}, []);
// What goes in the dependency array?
// - Everything from the component scope used inside the effect
// - Omitting deps is almost always a bug
// - use eslint-plugin-react-hooks to catch missing deps
In React 19 / future React, the use() hook + Suspense replaces most useEffect data fetching. Start migrating to React Query, SWR, or use() for new code.
useMemo and useCallback — when to actually use them
// useMemo — cache expensive computed value
const sorted = useMemo(
() => [...items].sort((a, b) => a.name.localeCompare(b.name)),
[items]
);
// useCallback — stable function reference
const handleSubmit = useCallback(async (data: FormData) => {
await api.post("/submit", data);
onSuccess();
}, [onSuccess]); // only recreated if onSuccess changes
// When to use them:
// useMemo: expensive computation (> 1ms), passed as dep to other hooks
// useCallback: stable ref needed for React.memo'd child, or as dep in useEffect
// When NOT to bother:
// - Simple transforms that run in microseconds
// - Values not passed to memoised children
// - "Just in case" — profile first, optimise second
// useTransition — keep UI responsive during heavy updates (React 18+)
const [isPending, startTransition] = useTransition();
function handleInput(e) {
setInputValue(e.target.value); // immediate — update input
startTransition(() => {
setFilteredResults(filter(allItems, e.target.value)); // deferred
});
}
// useDeferredValue — like useTransition but for values you don't control
const deferredQuery = useDeferredValue(query);
const results = useMemo(() => search(data, deferredQuery), [data, deferredQuery]);
Context — global state without prop drilling
// Define context with a sensible default and a provider hook
interface ThemeContextValue {
theme: "light" | "dark";
toggle: () => void;
}
const ThemeContext = React.createContext(null);
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<"light" | "dark">("light");
const value = useMemo(() => ({
theme,
toggle: () => setTheme(t => t === "light" ? "dark" : "light"),
}), [theme]);
return (
{children}
);
}
// Custom hook — validates usage and provides type safety
export function useTheme() {
const ctx = useContext(ThemeContext);
if (!ctx) throw new Error("useTheme must be used inside ThemeProvider");
return ctx;
}
// Composing multiple providers (common pattern)
function AppProviders({ children }) {
return (
{children}
);
}
// Context + useReducer — lightweight global state
const StateContext = createContext(null);
const DispatchContext = createContext(null);
export function useAppState() { return useContext(StateContext); }
export function useAppDispatch() { return useContext(DispatchContext); }
// Split context avoids re-renders in components that only dispatch
Custom hooks — reusable stateful logic
// useLocalStorage — persist state in localStorage
function useLocalStorage(key: string, initialValue: T) {
const [value, setValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch {
return initialValue;
}
});
const set = useCallback((v: T | ((prev: T) => T)) => {
setValue(prev => {
const next = typeof v === "function" ? (v as Function)(prev) : v;
localStorage.setItem(key, JSON.stringify(next));
return next;
});
}, [key]);
return [value, set] as const;
}
// useDebounce — delay updates (search inputs)
function useDebounce(value: T, delay: number): T {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debounced;
}
// useFetch — data fetching with loading/error state
function useFetch(url: string) {
const [state, setState] = useState<{
data: T | null; loading: boolean; error: Error | null;
}>({ data: null, loading: true, error: null });
useEffect(() => {
const controller = new AbortController();
fetch(url, { signal: controller.signal })
.then(r => { if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.json(); })
.then(data => setState({ data, loading: false, error: null }))
.catch(err => { if (err.name !== "AbortError") setState(s => ({ ...s, loading: false, error: err })); });
return () => controller.abort();
}, [url]);
return state;
}
// useEventListener — safe event listener management
function useEventListener(
event: string,
handler: (e: T) => void,
target: EventTarget = window
) {
const savedHandler = useRef(handler);
useEffect(() => { savedHandler.current = handler; }, [handler]);
useEffect(() => {
const fn = (e: Event) => savedHandler.current(e as T);
target.addEventListener(event, fn);
return () => target.removeEventListener(event, fn);
}, [event, target]);
}
Server Components and React 19
// React Server Components (Next.js App Router / React 19)
// Default in App Router — run on server, no client JS
// Server Component — async, can fetch directly
async function UserProfile({ id }: { id: string }) {
const user = await db.users.findUnique({ where: { id } }); // direct DB access
if (!user) notFound();
return (
{user.name}
}>
{/* another async Server Component */}
);
}
// Client Component — needs interactivity, browser APIs
"use client";
function Counter() {
const [count, setCount] = useState(0);
return ;
}
// Pass Server data to Client Component
async function Page() {
const initialData = await fetchData();
return ; // data serialised to client
}
// use() hook (React 19) — consume Promises and Context anywhere
import { use } from "react";
function UserCard({ userPromise }: { userPromise: Promise }) {
const user = use(userPromise); // Suspense boundary handles loading
return {user.name};
}
// Server Actions — mutate data from Client Components
"use server";
async function updateUser(formData: FormData) {
const name = formData.get("name") as string;
await db.users.update({ where: { id: getSession().userId }, data: { name } });
revalidatePath("/profile");
}
// Use in form
Performance — profiling and common fixes
// Code splitting — lazy load components
const HeavyChart = lazy(() => import("./HeavyChart"));
function Dashboard() {
return (
}>
);
}
// Virtualise long lists — only render visible rows
import { FixedSizeList } from "react-window";
{({ index, style }) =>
}
// Avoid creating objects/arrays in JSX (causes child re-renders)
// BAD
// GOOD
const STYLE = { color: "red" };
const OPTIONS = ["a", "b"];
// Key prop — stable, unique identifiers
// BAD: index as key — breaks when list order changes
{items.map((item, i) => )}
// GOOD: stable ID
{items.map(item => )}
// React DevTools Profiler — find re-renders
// Components → gear icon → "Highlight updates when components render"
// Why did this render? (react-scan or Why Did You Render)
// npm install @welldone-software/why-did-you-render
Component.whyDidYouRender = true;
// Measure render performance
import { unstable_trace as trace } from "scheduler/tracing";
trace("click", performance.now(), () => dispatch(action));
Error boundaries and Suspense
// Error Boundary — catch render errors (still needs class component in React 18)
class ErrorBoundary extends React.Component {
state = { hasError: false, error: null };
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, info) {
reportError(error, info.componentStack);
}
render() {
if (this.state.hasError) {
return this.props.fallback ?? Something went wrong.;
}
return this.props.children;
}
}
// react-error-boundary package (recommended — simpler API)
import { ErrorBoundary } from "react-error-boundary";
(
Error: {error.message}
)}
onError={(error, info) => logToSentry(error, info)}
>
// Suspense — show fallback while async content loads
}>
// Nested Suspense — granular loading states
}>
}>
}>
🔍 Free tool: npm Package Health Checker — check React and its ecosystem packages for EOL status, known CVEs, and latest versions before upgrading.
Founded
2023 in London, UK
Contact
hello@releaserun.com