|$ curl https://forge-ai.dev/api/markdown?path=docs/react/patterns
$cat docs/patterns.md
updated Recently·45 min read·published

Patterns

ReactPatternsArchitectureIntermediate to Advanced🎯Free Tools
Introduction

Design patterns are proven solutions to recurring problems in software design. In React, patterns help you organize components, share logic, manage complexity, and build flexible, maintainable APIs. Understanding these patterns lets you choose the right abstraction for each situation.

The patterns evolved alongside React itself. Class component patterns (HOCs, render props) have been largely replaced by hooks-based patterns (custom hooks, compound components). This guide covers both — legacy patterns you'll encounter in existing codebases and modern patterns you should use in new projects.

Custom Hooks Pattern

The custom hooks pattern is the primary way to share stateful logic in modern React. It extracts reusable logic into standalone functions that can be composed together. Custom hooks are the most important pattern to master.

custom-hooks-pattern.jsx
JSX
1// Custom hook: encapsulates complex logic
2function useOnlineStatus() {
3 const [isOnline, setIsOnline] = useState(navigator.onLine);
4
5 useEffect(() => {
6 const goOnline = () => setIsOnline(true);
7 const goOffline = () => setIsOnline(false);
8
9 window.addEventListener("online", goOnline);
10 window.addEventListener("offline", goOffline);
11
12 return () => {
13 window.removeEventListener("online", goOnline);
14 window.removeEventListener("offline", goOffline);
15 };
16 }, []);
17
18 return isOnline;
19}
20
21// Custom hook: API call with loading/error states
22function useApi(url) {
23 const [state, setState] = useState({
24 data: null,
25 loading: true,
26 error: null,
27 });
28
29 useEffect(() => {
30 let cancelled = false;
31 setState(prev => ({ ...prev, loading: true }));
32
33 fetch(url)
34 .then(res => {
35 if (!res.ok) throw new Error(`HTTP ${res.status}`);
36 return res.json();
37 })
38 .then(data => {
39 if (!cancelled) setState({ data, loading: false, error: null });
40 })
41 .catch(error => {
42 if (!cancelled) setState({ data: null, loading: false, error });
43 });
44
45 return () => { cancelled = true; };
46 }, [url]);
47
48 return state;
49}
50
51// Composing hooks together
52function UserProfile({ userId }) {
53 const isOnline = useOnlineStatus();
54 const { data: user, loading, error } = useApi(`/api/users/${userId}`);
55
56 if (loading) return <Spinner />;
57 if (error) return <ErrorMessage error={error} />;
58
59 return (
60 <div>
61 <h2>{user.name}</h2>
62 {isOnline && <span className="green-dot" />}
63 <p>{user.bio}</p>
64 </div>
65 );
66}
67
68// Complex hook: form with validation
69function useLoginForm() {
70 const [values, setValues] = useState({ email: "", password: "" });
71 const [errors, setErrors] = useState({});
72 const [isSubmitting, setIsSubmitting] = useState(false);
73
74 const validate = () => {
75 const newErrors = {};
76 if (!values.email) newErrors.email = "Required";
77 else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(values.email)) {
78 newErrors.email = "Invalid email";
79 }
80 if (!values.password) newErrors.password = "Required";
81 else if (values.password.length < 8) newErrors.password = "Too short";
82 setErrors(newErrors);
83 return Object.keys(newErrors).length === 0;
84 };
85
86 const handleChange = (e) => {
87 const { name, value } = e.target;
88 setValues(prev => ({ ...prev, [name]: value }));
89 if (errors[name]) setErrors(prev => ({ ...prev, [name]: undefined }));
90 };
91
92 const handleSubmit = async (onSubmit) => async (e) => {
93 e.preventDefault();
94 if (!validate()) return;
95 setIsSubmitting(true);
96 try {
97 await onSubmit(values);
98 } finally {
99 setIsSubmitting(false);
100 }
101 };
102
103 return { values, errors, isSubmitting, handleChange, handleSubmit };
104}
🔥

pro tip

Custom hooks are the most versatile pattern. Before creating a HOC, render prop, or context, ask yourself: "Can this be a custom hook?" Usually the answer is yes. Custom hooks are simpler, more composable, and have better TypeScript support.
Compound Components

Compound components share implicit state through Context, creating an expressive, declarative API. The parent manages state and children access it through Context — consumers don't need to pass props manually.

compound-components.jsx
JSX
1import { createContext, useContext, useState } from "react";
2
3// Step 1: Create context
4const TabsContext = createContext(null);
5
6// Step 2: Parent component manages state
7function Tabs({ children, defaultValue }) {
8 const [activeTab, setActiveTab] = useState(defaultValue);
9
10 return (
11 <TabsContext.Provider value={{ activeTab, setActiveTab }}>
12 <div className="tabs">{children}</div>
13 </TabsContext.Provider>
14 );
15}
16
17// Step 3: Child components use context
18Tabs.List = function TabsList({ children }) {
19 return <div className="tab-list" role="tablist">{children}</div>;
20};
21
22Tabs.Tab = function Tab({ value, children }) {
23 const { activeTab, setActiveTab } = useContext(TabsContext);
24 const isActive = activeTab === value;
25
26 return (
27 <button
28 role="tab"
29 aria-selected={isActive}
30 className={`tab ${isActive ? "active" : ""}`}
31 onClick={() => setActiveTab(value)}
32 >
33 {children}
34 </button>
35 );
36};
37
38Tabs.Panel = function TabPanel({ value, children }) {
39 const { activeTab } = useContext(TabsContext);
40
41 if (activeTab !== value) return null;
42
43 return (
44 <div role="tabpanel" className="tab-panel">
45 {children}
46 </div>
47 );
48};
49
50// Step 4: Clean, declarative usage
51function App() {
52 return (
53 <Tabs defaultValue="overview">
54 <Tabs.List>
55 <Tabs.Tab value="overview">Overview</Tabs.Tab>
56 <Tabs.Tab value="details">Details</Tabs.Tab>
57 <Tabs.Tab value="reviews">Reviews</Tabs.Tab>
58 </Tabs.List>
59 <Tabs.Panel value="overview">Overview content here</Tabs.Panel>
60 <Tabs.Panel value="details">Details content here</Tabs.Panel>
61 <Tabs.Panel value="reviews">Reviews content here</Tabs.Panel>
62 </Tabs>
63 );
64}
65
66// Another example: Disclosure (accordion)
67const DisclosureContext = createContext(null);
68
69function Disclosure({ children, defaultOpen = false }) {
70 const [isOpen, setIsOpen] = useState(defaultOpen);
71 const toggle = () => setIsOpen(prev => !prev);
72
73 return (
74 <DisclosureContext.Provider value={{ isOpen, toggle }}>
75 <div className="disclosure">{children}</div>
76 </DisclosureContext.Provider>
77 );
78}
79
80Disclosure.Button = function DisclosureButton({ children }) {
81 const { isOpen, toggle } = useContext(DisclosureContext);
82 return (
83 <button onClick={toggle} aria-expanded={isOpen}>
84 {children}
85 </button>
86 );
87};
88
89Disclosure.Panel = function DisclosurePanel({ children }) {
90 const { isOpen } = useContext(DisclosureContext);
91 return isOpen ? <div className="panel">{children}</div> : null;
92};

best practice

Compound components are ideal when you have a group of related components that share state (Tabs, Accordion, Menu, Steps, Form fields). They provide a clean API, enforce correct usage, and hide internal complexity.
Provider Pattern

The provider pattern wraps the component tree with context to provide shared state, services, or configuration. It's the foundation for themes, authentication, localization, and state management.

provider-pattern.jsx
JSX
1import { createContext, useContext, useState, useEffect } from "react";
2
3// Auth provider — manages authentication state
4const AuthContext = createContext(null);
5
6export function AuthProvider({ children }) {
7 const [user, setUser] = useState(null);
8 const [loading, setLoading] = useState(true);
9
10 useEffect(() => {
11 // Check for existing session on mount
12 const session = localStorage.getItem("session");
13 if (session) {
14 setUser(JSON.parse(session));
15 }
16 setLoading(false);
17 }, []);
18
19 const login = async (email, password) => {
20 const response = await fetch("/api/login", {
21 method: "POST",
22 headers: { "Content-Type": "application/json" },
23 body: JSON.stringify({ email, password }),
24 });
25 if (!response.ok) throw new Error("Login failed");
26 const { user, token } = await response.json();
27 localStorage.setItem("session", JSON.stringify(user));
28 localStorage.setItem("token", token);
29 setUser(user);
30 };
31
32 const logout = () => {
33 localStorage.removeItem("session");
34 localStorage.removeItem("token");
35 setUser(null);
36 };
37
38 const value = {
39 user,
40 isAuthenticated: !!user,
41 isLoading: loading,
42 login,
43 logout,
44 };
45
46 return (
47 <AuthContext.Provider value={value}>
48 {children}
49 </AuthContext.Provider>
50 );
51}
52
53// Custom hook for consuming context
54export function useAuth() {
55 const context = useContext(AuthContext);
56 if (!context) throw new Error("useAuth must be used within AuthProvider");
57 return context;
58}
59
60// Theme provider with nested context
61const ThemeContext = createContext(null);
62const LocaleContext = createContext(null);
63
64export function AppProviders({ children }) {
65 return (
66 <AuthProvider>
67 <ThemeProvider>
68 <LocaleProvider>
69 {children}
70 </LocaleProvider>
71 </ThemeProvider>
72 </AuthProvider>
73 );
74}
75
76// Composition in App.tsx
77function App() {
78 return (
79 <AppProviders>
80 <BrowserRouter>
81 <AppRoutes />
82 </BrowserRouter>
83 </AppProviders>
84 );
85}
86
87// Consuming providers in components
88function Header() {
89 const { user, logout } = useAuth();
90 const { theme, toggleTheme } = useTheme();
91 const { locale, setLocale } = useLocale();
92
93 return (
94 <header>
95 {user && <span>Welcome, {user.name}</span>}
96 <button onClick={toggleTheme}>{theme}</button>
97 <select value={locale} onChange={e => setLocale(e.target.value)}>
98 <option value="en">English</option>
99 <option value="es">Spanish</option>
100 </select>
101 {user && <button onClick={logout}>Logout</button>}
102 </header>
103 );
104}

info

Nest providers in a dedicated AppProviders component rather than in the root layout. This keeps the provider tree clean and makes it easy to add/remove providers. Consider using Zustand instead of Context for state that changes frequently.
Higher-Order Components (HOC)

A HOC is a function that takes a component and returns a new component with enhanced behavior. HOCs were the primary pattern for code reuse before hooks. They're still found in libraries like Redux (connect) and React Router.

hoc-pattern.jsx
JSX
1import { Component } from "react";
2
3// HOC: adds loading state to any component
4function withLoading(WrappedComponent) {
5 return function WithLoadingComponent({ isLoading, ...props }) {
6 if (isLoading) {
7 return <div className="spinner">Loading...</div>;
8 }
9 return <WrappedComponent {...props} />;
10 };
11}
12
13// HOC: adds authentication check
14function withAuth(WrappedComponent) {
15 return function WithAuthComponent(props) {
16 const { isAuthenticated, isLoading } = useAuth();
17
18 if (isLoading) return <Spinner />;
19 if (!isAuthenticated) return <Navigate to="/login" />;
20
21 return <WrappedComponent {...props} />;
22 };
23}
24
25// HOC: adds error boundary
26function withErrorBoundary(WrappedComponent, FallbackComponent) {
27 return class WithErrorBoundary extends Component {
28 state = { hasError: false, error: null };
29
30 static getDerivedStateFromError(error) {
31 return { hasError: true, error };
32 }
33
34 render() {
35 if (this.state.hasError) {
36 return <FallbackComponent error={this.state.error} />;
37 }
38 return <WrappedComponent {...this.props} />;
39 }
40 };
41}
42
43// HOC: adds logging
44function withLogging(WrappedComponent, componentName) {
45 return function WithLoggingComponent(props) {
46 useEffect(() => {
47 console.log(`${componentName} mounted`, props);
48 return () => console.log(`${componentName} unmounted`);
49 }, []);
50
51 return <WrappedComponent {...props} />;
52 };
53}
54
55// Usage — composing HOCs
56const EnhancedDashboard = withErrorBoundary(
57 withAuth(
58 withLoading(Dashboard)
59 ),
60 ErrorFallback
61);
62
63// HOC with data fetching
64function withUserData(WrappedComponent) {
65 return function WithUserData(props) {
66 const { data: user, loading, error } = useFetch("/api/me");
67
68 if (loading) return <Spinner />;
69 if (error) return <ErrorMessage error={error} />;
70
71 return <WrappedComponent user={user} {...props} />;
72 };
73}
74
75const UserDashboard = withUserData(Dashboard);
📝

note

HOCs are largely replaced by custom hooks. Use custom hooks for new code. Know HOCs for understanding legacy codebases and library APIs (Redux connect, withRouter). HOCs can cause "wrapper hell" — deeply nested components that are hard to debug.
Render Props Pattern

The render props pattern passes a function as a prop that the component calls to determine what to render. It gives parent components control over rendering while the child manages behavior. Largely replaced by custom hooks.

render-props-pattern.jsx
JSX
1// Render prop: manages mouse position
2function MouseTracker({ render }) {
3 const [position, setPosition] = useState({ x: 0, y: 0 });
4
5 useEffect(() => {
6 const handler = (e) => setPosition({ x: e.clientX, y: e.clientY });
7 window.addEventListener("mousemove", handler);
8 return () => window.removeEventListener("mousemove", handler);
9 }, []);
10
11 return render(position);
12}
13
14// Usage
15<MouseTracker render={({ x, y }) => <p>Mouse: {x}, {y}</p>} />
16
17// Render prop: manages window size
18function WindowSize({ render }) {
19 const [size, setSize] = useState({
20 width: window.innerWidth,
21 height: window.innerHeight,
22 });
23
24 useEffect(() => {
25 const handler = () => setSize({
26 width: window.innerWidth,
27 height: window.innerHeight,
28 });
29 window.addEventListener("resize", handler);
30 return () => window.removeEventListener("resize", handler);
31 }, []);
32
33 return render(size);
34}
35
36// Render prop: manages toggle state
37function Toggle({ initial = false, onToggle, children }) {
38 const [on, setOn] = useState(initial);
39
40 const toggle = () => {
41 const next = !on;
42 setOn(next);
43 onToggle?.(next);
44 };
45
46 return children({ on, toggle });
47}
48
49// Usage — flexible rendering
50<Toggle initial={false} onToggle={console.log}>
51 {({ on, toggle }) => (
52 <div>
53 <button onClick={toggle}>{on ? "ON" : "OFF"}</button>
54 {on && <ExpensiveContent />}
55 </div>
56 )}
57</Toggle>
58
59// Render prop: manages fetch state
60function Fetch({ url, children }) {
61 const { data, loading, error } = useFetch(url);
62 return children({ data, loading, error });
63}
64
65// Usage
66<Fetch url="/api/products">
67 {({ data, loading, error }) => {
68 if (loading) return <Spinner />;
69 if (error) return <Error error={error} />;
70 return <ProductList products={data} />;
71 }}
72</Fetch>
📝

note

Render props are largely replaced by custom hooks. A custom hook like useMouse() is simpler than a <MouseTracker render={...} /> component. Use render props only when you need to share behavior between multiple render trees without hooks.
Controlled vs Uncontrolled

The controlled vs uncontrolled pattern determines who owns the state — the parent (controlled) or the component itself (uncontrolled). This applies to inputs, modals, dropdowns, and any stateful component.

controlled-uncontrolled.jsx
JSX
1// Controlled component — parent owns the state
2function ControlledInput({ value, onChange, label }) {
3 return (
4 <div>
5 <label>{label}</label>
6 <input value={value} onChange={e => onChange(e.target.value)} />
7 </div>
8 );
9}
10
11// Uncontrolled component — component owns the state
12function UncontrolledInput({ defaultValue = "", label }) {
13 const [value, setValue] = useState(defaultValue);
14 return (
15 <div>
16 <label>{label}</label>
17 <input value={value} onChange={e => setValue(e.target.value)} />
18 </div>
19 );
20}
21
22// Flexible component supporting both patterns
23function FlexibleInput({
24 value, // controlled
25 defaultValue, // uncontrolled
26 onChange,
27 label,
28}) {
29 const [internalValue, setInternalValue] = useState(defaultValue || "");
30 const isControlled = value !== undefined;
31 const currentValue = isControlled ? value : internalValue;
32
33 const handleChange = (e) => {
34 const newValue = e.target.value;
35 if (!isControlled) setInternalValue(newValue);
36 onChange?.(e);
37 };
38
39 return (
40 <div>
41 <label>{label}</label>
42 <input value={currentValue} onChange={handleChange} />
43 </div>
44 );
45}
46
47// Usage
48// Controlled
49<FlexibleInput value={name} onChange={setName} label="Name" />
50
51// Uncontrolled
52<FlexibleInput defaultValue="hello" label="Name" />
53
54// Modal with controlled/uncontrolled pattern
55function Modal({ open, defaultOpen, onClose, children }) {
56 const [internalOpen, setInternalOpen] = useState(defaultOpen || false);
57 const isOpen = open !== undefined ? open : internalOpen;
58
59 const handleClose = () => {
60 if (open === undefined) setInternalOpen(false);
61 onClose?.();
62 };
63
64 return isOpen ? (
65 <div className="modal-overlay" onClick={handleClose}>
66 <div className="modal" onClick={e => e.stopPropagation()}>
67 {children}
68 <button onClick={handleClose}>Close</button>
69 </div>
70 </div>
71 ) : null;
72}
🔥

pro tip

Support both controlled and uncontrolled usage in your component APIs. Check if the prop is undefined to determine the mode. This gives consumers flexibility — controlled for complex state management, uncontrolled for simple cases.
Layout & Slot Pattern

The layout pattern provides a consistent structure across pages. The slot pattern (named children) lets consumers fill specific areas of a layout. Both are fundamental to building maintainable UIs.

layout-pattern.jsx
JSX
1// Layout pattern — shared structure
2function PageLayout({ children, sidebar }) {
3 return (
4 <div className="flex min-h-screen">
5 {sidebar && <aside className="w-64">{sidebar}</aside>}
6 <main className="flex-1">{children}</main>
7 </div>
8 );
9}
10
11// Slot pattern — named content areas
12function Card({ header, children, footer, actions }) {
13 return (
14 <div className="card">
15 {header && <div className="card-header">{header}</div>}
16 <div className="card-body">{children}</div>
17 {actions && <div className="card-actions">{actions}</div>}
18 {footer && <div className="card-footer">{footer}</div>}
19 </div>
20 );
21}
22
23// Usage
24<Card
25 header={<h2>User Profile</h2>}
26 actions={<Button>Edit</Button>}
27 footer={<p className="text-sm text-gray-400">Last updated: today</p>}
28>
29 <p>Name: Jane Doe</p>
30 <p>Email: jane@example.com</p>
31</Card>
32
33// Slot pattern: compound layout
34function DashboardLayout({ header, sidebar, content, footer }) {
35 return (
36 <div className="min-h-screen flex flex-col">
37 <header className="h-16 border-b">{header}</header>
38 <div className="flex flex-1">
39 <aside className="w-64 border-r">{sidebar}</aside>
40 <main className="flex-1 p-6">{content}</main>
41 </div>
42 <footer className="h-12 border-t">{footer}</footer>
43 </div>
44 );
45}
46
47// Flexible component with render props for slots
48function DataCard({ title, data, renderHeader, renderBody, renderFooter }) {
49 return (
50 <div className="data-card">
51 <div className="card-header">
52 {renderHeader ? renderHeader(data) : <h3>{title}</h3>}
53 </div>
54 <div className="card-body">
55 {renderBody ? renderBody(data) : <pre>{JSON.stringify(data, null, 2)}</pre>}
56 </div>
57 {renderFooter && <div className="card-footer">{renderFooter(data)}</div>}
58 </div>
59 );
60}
61
62// Usage
63<DataCard
64 title="User Data"
65 data={user}
66 renderHeader={(data) => <h3>{data.name}</h3>}
67 renderBody={(data) => (
68 <div>
69 <p>Email: {data.email}</p>
70 <p>Role: {data.role}</p>
71 </div>
72 )}
73 renderFooter={(data) => <span>Joined {data.joinDate}</span>}
74/>
State Machine Pattern

State machines eliminate impossible states by defining explicit states and transitions. They make complex state logic predictable and debuggable. Libraries like XState formalize this pattern.

state-machine.jsx
JSX
1// Simple state machine without XState
2const fetchStates = {
3 idle: {
4 FETCH: "loading",
5 },
6 loading: {
7 SUCCESS: "success",
8 FAILURE: "error",
9 },
10 success: {
11 FETCH: "loading",
12 RESET: "idle",
13 },
14 error: {
15 RETRY: "loading",
16 RESET: "idle",
17 },
18};
19
20function useFetchMachine(url) {
21 const [state, setState] = useState("idle");
22 const [data, setData] = useState(null);
23 const [error, setError] = useState(null);
24
25 const send = (event) => {
26 const nextState = fetchStates[state]?.[event];
27 if (!nextState) return;
28
29 setState(nextState);
30
31 if (event === "FETCH") {
32 fetch(url)
33 .then(res => res.json())
34 .then(data => {
35 setData(data);
36 setState("success");
37 })
38 .catch(err => {
39 setError(err);
40 setState("error");
41 });
42 }
43 };
44
45 return { state, data, error, send };
46}
47
48// Usage
49function DataFetcher({ url }) {
50 const { state, data, error, send } = useFetchMachine(url);
51
52 return (
53 <div>
54 {state === "idle" && <button onClick={() => send("FETCH")}>Load</button>}
55 {state === "loading" && <Spinner />}
56 {state === "success" && <DataView data={data} />}
57 {state === "error" && (
58 <div>
59 <p>Error: {error.message}</p>
60 <button onClick={() => send("RETRY")}>Retry</button>
61 </div>
62 )}
63 </div>
64 );
65}
66
67// With XState (more powerful)
68import { createMachine, assign } from "xstate";
69import { useMachine } from "@xstate/react";
70
71const todoMachine = createMachine({
72 id: "todos",
73 initial: "idle",
74 context: { todos: [], error: null },
75 states: {
76 idle: {
77 on: { FETCH: "loading" },
78 },
79 loading: {
80 invoke: {
81 src: "fetchTodos",
82 onDone: { target: "idle", actions: assign({ todos: (_, e) => e.data }) },
83 onError: { target: "error", actions: assign({ error: (_, e) => e.data }) },
84 },
85 },
86 error: {
87 on: { RETRY: "loading" },
88 },
89 },
90});
Composition Over Inheritance

React embraces composition over inheritance. Instead of extending components, compose them. This leads to more flexible, testable, and reusable code.

composition-pattern.jsx
JSX
1// ❌ Bad: inheritance approach (doesn't work in React)
2class BaseButton extends React.Component {
3 render() {
4 return <button className="btn">{this.props.children}</button>;
5 }
6}
7
8class PrimaryButton extends BaseButton { // Can't extend function components!
9 render() {
10 return <button className="btn btn-primary">{this.props.children}</button>;
11 }
12}
13
14// ✅ Good: composition approach
15function Button({ variant = "default", size = "md", children, ...props }) {
16 const classes = `btn btn-${variant} btn-${size}`;
17 return <button className={classes} {...props}>{children}</button>;
18}
19
20// ✅ Good: wrapper pattern for extending behavior
21function withIcon(IconComponent) {
22 return function ButtonWithIcon({ iconPosition = "left", children, ...props }) {
23 return (
24 <Button {...props}>
25 {iconPosition === "left" && <IconComponent />}
26 {children}
27 {iconPosition === "right" && <IconComponent />}
28 </Button>
29 );
30 };
31}
32
33const SearchButton = withIcon(SearchIcon);
34
35// ✅ Good: composition for complex components
36function StatCard({ icon, label, value, trend, trendValue }) {
37 return (
38 <Card>
39 <CardHeader>
40 <Icon name={icon} />
41 <span>{label}</span>
42 </CardHeader>
43 <CardBody>
44 <StatValue value={value} />
45 <TrendIndicator trend={trend} value={trendValue} />
46 </CardBody>
47 </Card>
48 );
49}
50
51// ✅ Good: render delegation
52function List({ items, renderItem, renderEmpty, keyExtractor }) {
53 if (items.length === 0) {
54 return renderEmpty ? renderEmpty() : <p>No items</p>;
55 }
56
57 return (
58 <ul>
59 {items.map((item, index) => (
60 <li key={keyExtractor ? keyExtractor(item) : index}>
61 {renderItem(item, index)}
62 </li>
63 ))}
64 </ul>
65 );
66}

best practice

Never use inheritance to share behavior between React components. Use composition (wrapping), custom hooks (shared logic), or HOCs (enhanced components). Composition makes components more flexible and easier to test.