React Patterns Basics
11 examples to get you started with React Patterns -- 7 basic and 4 intermediate.
Search across all documentation pages
11 examples to get you started with React Patterns -- 7 basic and 4 intermediate.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
No extra packages required -- every pattern on this page ships with React. A standard React project (Next.js, Vite, or CRA) with TypeScript is enough.
The patterns below are ways of thinking about components -- how they compose, how they share state, where to put loading/error boundaries. They are not APIs you install; they are reusable shapes you recognize.
Build layout and wrapper components by accepting children instead of subclassing or configuring.
function Card({ children }: { children: React.ReactNode }) {
return <div className="border rounded p-4 shadow">{children}</div>;
}
// Usage
function Page() {
return (
<Card>
<h2>Title</h2>
<p>Any content the parent wants to pass in.</p>
</Card>
);
}children is whatever you put between the component's opening and closing tags.React.ReactNode accepts strings, numbers, elements, fragments, or null -- the widest "renderable" type.<Card header={...} footer={...} />.Related: Composition Over Inheritance -- slots, compound components, layout patterns | Components (Fundamentals) -- the
childrenprop in depth
Decide who owns the component's state: the parent (controlled) or the component itself (uncontrolled).
import { useState } from "react";
// Controlled: parent owns the value
function ControlledInput({
value, onChange,
}: {
value: string;
onChange: (v: string) => void;
}) {
return <input value={value} onChange={(e) => onChange(e.target.value)} />;
}
// Uncontrolled: component owns its own state internally
function UncontrolledInput({ defaultValue = "" }: { defaultValue?: string }) {
const [value, setValue] = useState(defaultValue);
return <input value={value} onChange={(e) => setValue(e.target.value)} />;
}value/onChange (controlled) or defaultValue (uncontrolled), never mix them.Related: Controlled vs Uncontrolled -- API shape, both-modes components, edge cases | Forms: Controlled vs Uncontrolled -- the form-specific side
Share behavior by passing a function as a child or prop, returning JSX based on internal state.
import { useState } from "react";
function Toggle({
render,
}: {
render: (state: { on: boolean; toggle: () => void }) => React.ReactNode;
}) {
const [on, setOn] = useState(false);
return <>{render({ on, toggle: () => setOn((v) => !v) })}</>;
}
// Usage
function App() {
return (
<Toggle
render={({ on, toggle }) => (
<button onClick={toggle}>{on ? "ON" : "OFF"}</button>
)}
/>
);
}render, children, or something descriptive (renderItem); be consistent within a codebase.Related: Render Props -- when to still use them, hooks alternative | Higher-Order Components -- a sibling indirection pattern
Design multi-part components that share implicit state through context -- the parent coordinates, the children compose.
import { createContext, useContext, useState } from "react";
const TabsCtx = createContext<{ active: string; setActive: (v: string) => void; } | null>(null);
function Tabs({ defaultValue, children }: { defaultValue: string; children: React.ReactNode }) {
const [active, setActive] = useState(defaultValue);
return <TabsCtx.Provider value={{ active, setActive }}>{children}</TabsCtx.Provider>;
}
function Tab({ value, children }: { value: string; children: React.ReactNode }) {
const ctx = useContext(TabsCtx)!;
return (
<button onClick={() => ctx.setActive(value)} aria-pressed={ctx.active === value}>
{children}
</button>
);
}
function Panel({ value, children }: { value: string; children: React.ReactNode }) {
const ctx = useContext(TabsCtx)!;
return ctx.active === value ? <div>{children}</div> : null;
}
// Usage
// <Tabs defaultValue="a">
// <Tab value="a">A</Tab><Tab value="b">B</Tab>
// <Panel value="a">Content A</Panel><Panel value="b">Content B</Panel>
// </Tabs>Tabs.Tab, Tabs.Panel) for a clean API.useContext returns null, so misuse fails loudly.Related: Compound Components -- deep dive, TypeScript typing | Context Patterns -- context best practices
Share data across a subtree without prop drilling; split contexts by update frequency to avoid over-rendering.
import { createContext, useContext, useMemo, useState } from "react";
type Theme = "light" | "dark";
const ThemeCtx = createContext<Theme>("light");
const SetThemeCtx = createContext<(t: Theme) => void>(() => {});
function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<Theme>("light");
const set = useMemo(() => setTheme, []);
return (
<ThemeCtx.Provider value={theme}>
<SetThemeCtx.Provider value={set}>{children}</SetThemeCtx.Provider>
</ThemeCtx.Provider>
);
}
function ThemeToggle() {
const theme = useContext(ThemeCtx);
const setTheme = useContext(SetThemeCtx);
return (
<button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
{theme}
</button>
);
}setTheme do not re-render when theme changes.useMemo or useCallback so the write context's value stays referentially stable.Related: Context Patterns -- splitting, optimization, server components | useContext -- the underlying hook | Context vs. Zustand -- when to use each
Catch render-time errors in a subtree and render a fallback instead of a white screen.
"use client";
import { Component, type ReactNode } from "react";
interface State { error: Error | null; }
class ErrorBoundary extends Component<{ children: ReactNode }, State> {
state: State = { error: null };
static getDerivedStateFromError(error: Error): State {
return { error };
}
componentDidCatch(error: Error) {
console.error("UI error:", error);
}
render() {
if (this.state.error) {
return <p role="alert">Something went wrong: {this.state.error.message}</p>;
}
return this.props.children;
}
}
// Usage
// <ErrorBoundary><BuggyChart /></ErrorBoundary>getDerivedStateFromError has no hook equivalent yet.react-error-boundary (lighter, hooks-friendly API) and log to Sentry/Datadog in componentDidCatch.Related: Error Boundaries -- react-error-boundary, Next.js
error.tsx, logging | Suspense -- the loading counterpart
Render children into a DOM node outside the parent's tree -- escape overflow: hidden and z-index traps.
"use client";
import { createPortal } from "react-dom";
function Modal({
open, onClose, children,
}: {
open: boolean;
onClose: () => void;
children: React.ReactNode;
}) {
if (!open || typeof document === "undefined") return null;
return createPortal(
<div
onClick={onClose}
style={{
position: "fixed", inset: 0, background: "rgba(0,0,0,0.5)",
display: "grid", placeItems: "center",
}}
>
<div onClick={(e) => e.stopPropagation()} style={{ background: "white", padding: 24 }}>
{children}
</div>
</div>,
document.body,
);
}createPortal(node, container) mounts node inside container, but events still bubble to the React parent.typeof document === "undefined" so it is safe in SSR; otherwise document throws on the server.Related: React Portals -- focus trap, accessibility, z-index recipes | Modal Component -- production modal patterns
Declaratively show a fallback while an async child resolves -- no isLoading prop passing.
"use client";
import { Suspense, use } from "react";
interface User { id: number; name: string; }
function UserCard({ userPromise }: { userPromise: Promise<User> }) {
const user = use(userPromise);
return <h2>{user.name}</h2>;
}
export default function UserPage({
userPromise,
}: {
userPromise: Promise<User>;
}) {
return (
<Suspense fallback={<p>Loading user...</p>}>
<UserCard userPromise={userPromise} />
</Suspense>
);
}<Suspense fallback={...}> catches child components that "suspend" (Server Components awaiting data, Client Components reading a promise with use).<ErrorBoundary> so rejected promises show an error UI rather than propagating up the tree.Related: Suspense Boundaries -- placement, streaming, edge cases | use hook -- reading promises and context | Next.js Streaming -- Suspense in Server Components
Wrap a component to inject shared behavior -- auth gating, logging, feature flags.
"use client";
import { useEffect } from "react";
function withLogging<P extends object>(
Component: React.ComponentType<P>,
label: string
) {
return function LoggedComponent(props: P) {
useEffect(() => {
console.log(`[${label}] mounted`);
return () => console.log(`[${label}] unmounted`);
}, []);
return <Component {...props} />;
};
}
// Usage
function Dashboard({ userId }: { userId: string }) {
return <p>Dashboard for {userId}</p>;
}
const LoggedDashboard = withLogging(Dashboard, "Dashboard");<P extends object>) so the wrapped component keeps its original prop types.function LoggedComponent(...)) so React DevTools shows something useful.Related: Higher-Order Components -- typing, gotchas, when to prefer hooks | Render Props -- the other "indirection" pattern
Model complex UI states as explicit transitions -- no more "impossible" prop combinations.
import { useReducer } from "react";
type Status =
| { kind: "idle" }
| { kind: "loading" }
| { kind: "success"; data: string }
| { kind: "error"; message: string };
type Event =
| { type: "FETCH" }
| { type: "RESOLVE"; data: string }
| { type: "REJECT"; message: string }
| { type: "RESET" };
function reducer(state: Status, event: Event): Status {
switch (state.kind) {
case "idle": return event.type === "FETCH" ? { kind: "loading" } : state;
case "loading": return event.type === "RESOLVE" ? { kind: "success", data: event.data }
: event.type === "REJECT" ? { kind: "error", message: event.message }
: state;
case "success":
case "error": return event.type === "RESET" ? { kind: "idle" } : state;
}
}
export default function DataPanel() {
const [state, dispatch] = useReducer(reducer, { kind: "idle" } as Status);
return (
<div>
{state.kind === "idle" && <button onClick={() => dispatch({ type: "FETCH" })}>Load</button>}
{state.kind === "loading" && <p>Loading...</p>}
{state.kind === "success" && <p>Got: {state.data}</p>}
{state.kind === "error" && <p>Error: {state.message}</p>}
</div>
);
}loading: true, error: "...", data: "x", isLoading: true) impossible.useTransition or useActionState to trigger state changes from async work.Related: State Machines for UI Logic -- deeper examples, XState integration | useReducer -- the hook behind it
Prevent a child from re-rendering when its parent re-renders but its props have not changed.
import { memo, useCallback, useState } from "react";
const Row = memo(function Row({
label, onSelect,
}: {
label: string;
onSelect: (label: string) => void;
}) {
return <li onClick={() => onSelect(label)}>{label}</li>;
});
export default function List({ items }: { items: string[] }) {
const [selected, setSelected] = useState<string | null>(null);
// Stable identity -- useCallback keeps memo'd rows from re-rendering
const onSelect = useCallback((label: string) => setSelected(label), []);
return (
<>
<p>Selected: {selected ?? "none"}</p>
<ul>
{items.map((item) => (
<Row key={item} label={item} onSelect={onSelect} />
))}
</ul>
</>
);
}memo(Component) skips re-renders when props are referentially equal to the previous render.useCallback gives it a stable identity so memo can actually short-circuit.memo.memo/useCallback/useMemo entirely -- the compiler handles it.Related: React Performance Optimization -- profiling, keys, list virtualization | Re-renders -- what triggers renders and how to cut them | React Compiler -- auto-memoization
Reviewed by Chris St. John·Last updated Jul 16, 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥