React Snippets (Design Patterns)¶
These examples assume a React environment (react installed, JSX enabled, React in scope). They intentionally cover only cases where React built-ins (Context, hooks like useReducer / useSyncExternalStore, memo, lazy, Suspense, cloneElement) change the most idiomatic implementation compared to plain TypeScript. For language-only examples, see references/snippets/typescript.md.
Creational Patterns¶
Omitted (not React-specific)¶
- Simple "factory method" component selection (switching between implementations) is usually just TypeScript control flow; see
references/snippets/typescript.md.
Contents¶
- Abstract Factory (design system via context)
- Builder (normalize/validate config)
- "Singleton" (app-wide service via context)
Abstract Factory (theme/design-system factory via context)¶
Use when you need a compatible family of components (Button, Link, Modal) that must change together (white-labeling, multi-brand).
import * as React from 'react';
type DesignSystem = {
Button: React.FC<React.ButtonHTMLAttributes<HTMLButtonElement>>;
Link: React.FC<React.AnchorHTMLAttributes<HTMLAnchorElement>>;
};
type Result<T, E> = { ok: true; value: T } | { ok: false; error: E };
const ok = <T,>(value: T): Result<T, never> => ({ ok: true, value });
const err = <E,>(error: E): Result<never, E> => ({ ok: false, error });
const DesignSystemContext = React.createContext<DesignSystem | null>(null);
type UseDesignSystemError = { kind: 'missing-design-system-provider' };
export const useDesignSystem = (): Result<DesignSystem, UseDesignSystemError> => {
const value = React.useContext(DesignSystemContext);
return value ? ok(value) : err({ kind: 'missing-design-system-provider' });
};
export const DesignSystemProvider = ({
create,
deps,
children,
}: {
create: () => DesignSystem;
deps: React.DependencyList;
children: React.ReactNode;
}) => {
// Factory Method + memoization: build the family once per dependency set.
const value = React.useMemo(create, deps);
return <DesignSystemContext.Provider value={value}>{children}</DesignSystemContext.Provider>;
};
Builder (reduce prop explosion with a typed config object)¶
Use when configuration has many optional parts and must be built stepwise with validation/defaults.
import * as React from 'react';
type Result<T, E> = { ok: true; value: T } | { ok: false; error: E };
const ok = <T,>(value: T): Result<T, never> => ({ ok: true, value });
const err = <E,>(error: E): Result<never, E> => ({ ok: false, error });
type QueryConfig = {
page: number;
pageSize: number;
sort?: 'name' | 'createdAt';
};
type BuildError =
| { kind: 'invalid-page'; page: number }
| { kind: 'invalid-page-size'; pageSize: number };
type Draft = {
page: number | null;
pageSize: number | null;
sort?: QueryConfig['sort'];
};
const defaults: Draft = { page: null, pageSize: null };
// Builder: chainable steps + one build() that enforces invariants.
const queryBuilder = (draft: Draft = defaults) => ({
withPage: (page: number) => queryBuilder({ ...draft, page }),
withPageSize: (pageSize: number) => queryBuilder({ ...draft, pageSize }),
withSort: (sort: QueryConfig['sort'] | undefined) => queryBuilder({ ...draft, sort }),
build: (): Result<QueryConfig, BuildError> => {
const page = draft.page ?? 1;
const pageSize = draft.pageSize ?? 25;
if (!Number.isInteger(page) || page < 1) {
return err({ kind: 'invalid-page', page });
}
if (!Number.isInteger(pageSize) || pageSize < 1 || pageSize > 100) {
return err({ kind: 'invalid-page-size', pageSize });
}
return ok({ page, pageSize, sort: draft.sort });
},
});
const buildQuery = (raw: Partial<QueryConfig>) =>
queryBuilder()
.withPage(raw.page ?? 1)
.withPageSize(raw.pageSize ?? 25)
.withSort(raw.sort)
.build();
export const Results = ({ rawConfig }: { rawConfig: Partial<QueryConfig> }) => {
const result = React.useMemo(() => buildQuery(rawConfig), [rawConfig]);
if (!result.ok) {
return <div>Invalid config: {result.error.kind}</div>;
}
return <div>page={result.value.page}</div>;
};
"Singleton" (app-wide service via Context)¶
Use when you need a single shared instance for the React tree (e.g., API client). Prefer Context + useMemo over module globals so tests and SSR are cleaner.
import * as React from 'react';
type Result<T, E> = { ok: true; value: T } | { ok: false; error: E };
const ok = <T,>(value: T): Result<T, never> => ({ ok: true, value });
const err = <E,>(error: E): Result<never, E> => ({ ok: false, error });
const toError = (value: unknown): Error => (value instanceof Error ? value : new Error(String(value)));
type ApiError =
| { kind: 'aborted' }
| { kind: 'network'; message: string }
| { kind: 'bad-status'; status: number }
| { kind: 'invalid-json'; message: string };
type ApiClient = {
getJson: (path: string, options?: { signal?: AbortSignal }) => Promise<Result<unknown, ApiError>>;
};
const ApiClientContext = React.createContext<ApiClient | null>(null);
export const ApiClientProvider = ({
baseUrl,
children,
}: {
baseUrl: string;
children: React.ReactNode;
}) => {
const client = React.useMemo<ApiClient>(() => {
return {
getJson: async (path, options) => {
try {
const res = await fetch(`${baseUrl}${path}`, { signal: options?.signal });
if (!res.ok) {
return err({ kind: 'bad-status', status: res.status });
}
try {
const json: unknown = await res.json();
return ok(json);
} catch (error) {
return err({ kind: 'invalid-json', message: toError(error).message });
}
} catch (error) {
const e = toError(error);
if (e.name === 'AbortError') {
return err({ kind: 'aborted' });
}
return err({ kind: 'network', message: e.message });
}
},
};
}, [baseUrl]);
return <ApiClientContext.Provider value={client}>{children}</ApiClientContext.Provider>;
};
type UseApiClientError = { kind: 'missing-api-client-provider' };
export const useApiClient = (): Result<ApiClient, UseApiClientError> => {
const value = React.useContext(ApiClientContext);
return value ? ok(value) : err({ kind: 'missing-api-client-provider' });
};
Structural Patterns¶
Omitted (not React-specific)¶
- Composite recursion over a tree and Adapter mapping between data shapes are usually plain TypeScript; see
references/snippets/typescript.md.
Contents¶
- Decorator (cloneElement)
- Facade (custom hook)
- Proxy (lazy + gating)
- Bridge (context-based implementation)
Decorator (wrap behavior with React built-ins)¶
Wrap a component to add behavior (concern stays outside the core component). In React this often looks like "decorate children" with React.cloneElement.
import * as React from 'react';
export const WithTracking = ({
eventName,
children,
}: {
eventName: string;
children: React.ReactElement<{ onClick?: React.MouseEventHandler }>;
}) => {
const child = React.Children.only(children);
const wrappedOnClick = React.useCallback<React.MouseEventHandler>(
(e) => {
console.log('track', eventName);
child.props.onClick?.(e);
},
[eventName, child.props.onClick],
);
return React.cloneElement(child, { onClick: wrappedOnClick });
};
Facade (custom hook hides a subsystem)¶
Hide data-fetching + caching + transformation behind a single hook.
import * as React from 'react';
type User = { id: string; name: string };
const isUser = (value: unknown): value is User => {
if (typeof value !== 'object' || value === null) {
return false;
}
const record = value as Record<string, unknown>;
return typeof record.id === 'string' && typeof record.name === 'string';
};
type UserProfileError =
| { kind: 'bad-status'; status: number }
| { kind: 'invalid-payload' }
| { kind: 'network'; message: string };
const toError = (value: unknown): Error => (value instanceof Error ? value : new Error(String(value)));
export const useUserProfile = (userId: string) => {
const [user, setUser] = React.useState<User | null>(null);
const [loading, setLoading] = React.useState(true);
const [error, setError] = React.useState<UserProfileError | null>(null);
React.useEffect(() => {
const controller = new AbortController();
setLoading(true);
setError(null);
(async () => {
try {
const res = await fetch(`/api/users/${userId}`, { signal: controller.signal });
if (!res.ok) {
setUser(null);
setError({ kind: 'bad-status', status: res.status });
return;
}
const json: unknown = await res.json();
if (!isUser(json)) {
setUser(null);
setError({ kind: 'invalid-payload' });
return;
}
setUser(json);
} catch (error) {
const caughtError = toError(error);
if (caughtError.name === 'AbortError') {
return;
}
setUser(null);
setError({ kind: 'network', message: caughtError.message });
} finally {
if (!controller.signal.aborted) {
setLoading(false);
}
}
})();
return () => controller.abort();
}, [userId]);
return { user, loading, error };
};
Proxy (lazy loading / gated access / memoization)¶
Use a component as a stand-in that controls access or defers loading.
import * as React from 'react';
const SettingsPage = React.lazy(() => import('./SettingsPage'));
type ViewAccess =
| { kind: 'allowed' }
| { kind: 'denied'; reason?: string };
export const SettingsRoute = ({ access }: { access: ViewAccess }) => {
if (access.kind === 'denied') {
return <div>Not authorized{access.reason ? `: ${access.reason}` : null}</div>;
}
return (
<React.Suspense fallback={<div>Loading…</div>}>
<SettingsPage />
</React.Suspense>
);
};
React.memo acts like a caching proxy for rendering:
import * as React from 'react';
export const memoize = <P,>(Component: React.FC<P>) => React.memo(Component);
Bridge (abstraction decoupled from implementation via injected "renderer")¶
Two axes: the stable "toast API" your app depends on (abstraction) vs how toasts are actually rendered/delivered (implementor). Context selects the implementor; callers only use the abstraction.
import * as React from 'react';
type ToastKind = 'info' | 'success' | 'error';
type ToastItem = {
id: string;
kind: ToastKind;
message: string;
durationMs: number;
};
// Implementor: low-level toast delivery (UI, native bridge, logging, etc).
export type ToastImplementor = {
push(toast: ToastItem): void;
dismiss(id: string): void;
};
// Abstraction: what the app uses. It can grow independently of the implementor.
export type ToastService = {
info(message: string, options?: { durationMs?: number }): void;
success(message: string, options?: { durationMs?: number }): void;
error(message: string, options?: { durationMs?: number }): void;
dismiss(id: string): void;
};
type Result<T, E> = { ok: true; value: T } | { ok: false; error: E };
const ok = <T,>(value: T): Result<T, never> => ({ ok: true, value });
const err = <E,>(error: E): Result<never, E> => ({ ok: false, error });
const ToastImplContext = React.createContext<ToastImplementor | null>(null);
export const ToastProvider = ({
implementor,
children,
}: {
implementor: ToastImplementor;
children: React.ReactNode;
}) => <ToastImplContext.Provider value={implementor}>{children}</ToastImplContext.Provider>;
const createToastService = (implementor: ToastImplementor): ToastService => {
const pushToast = ({ kind, message, durationMs = 4000 }: { kind: ToastKind; message: string; durationMs?: number }) => {
const id = globalThis.crypto?.randomUUID?.() ?? `t_${Date.now()}_${Math.random().toString(16).slice(2)}`;
implementor.push({ id, kind, message, durationMs });
};
return {
info: (message, options) => pushToast({ kind: 'info', message, durationMs: options?.durationMs }),
success: (message, options) => pushToast({ kind: 'success', message, durationMs: options?.durationMs }),
error: (message, options) => pushToast({ kind: 'error', message, durationMs: options?.durationMs }),
dismiss: (id) => implementor.dismiss(id),
};
};
type UseToastError = { kind: 'missing-toast-provider' };
export const useToast = (): Result<ToastService, UseToastError> => {
const implementor = React.useContext(ToastImplContext);
return React.useMemo(() => {
if (!implementor) {
return err({ kind: 'missing-toast-provider' });
}
return ok(createToastService(implementor));
}, [implementor]);
};
// Implementor #1: in-app host that renders toasts (common in web apps).
export const ToastHost = ({ children }: { children: React.ReactNode }) => {
const [toasts, setToasts] = React.useState<ToastItem[]>([]);
const timeoutsRef = React.useRef<Map<string, ReturnType<typeof globalThis.setTimeout>>>(new Map());
React.useEffect(() => {
return () => {
for (const timeoutId of timeoutsRef.current.values()) {
globalThis.clearTimeout(timeoutId);
}
timeoutsRef.current.clear();
};
}, []);
const dismiss = React.useCallback((id: string) => {
const timeoutId = timeoutsRef.current.get(id);
if (timeoutId !== undefined) {
globalThis.clearTimeout(timeoutId);
timeoutsRef.current.delete(id);
}
setToasts((existingToasts) => existingToasts.filter((toast) => toast.id !== id));
}, []);
const implementor = React.useMemo<ToastImplementor>(
() => ({
push: (toast) => {
setToasts((existingToasts) => [...existingToasts, toast]);
const timeoutId = globalThis.setTimeout(() => dismiss(toast.id), toast.durationMs);
timeoutsRef.current.set(toast.id, timeoutId);
},
dismiss,
}),
[dismiss],
);
return (
<ToastProvider implementor={implementor}>
{children}
<div role="region" aria-label="Notifications" style={{ position: 'fixed', bottom: 12, right: 12 }}>
{toasts.map((toast) => (
<div key={toast.id} role="status">
<strong>{toast.kind}</strong> {toast.message}
<button onClick={() => dismiss(toast.id)}>Dismiss</button>
</div>
))}
</div>
</ToastProvider>
);
};
// Implementor #2: swap the renderer for tests/SSR/CLI without changing call sites.
export const createConsoleToastImplementor = (): ToastImplementor => ({
push: (toast) => console.log('[toast]', toast.kind, toast.message),
dismiss: () => {},
});
Now the rest of the app depends on the ToastService abstraction (via useToast()), while implementations vary by provider (implementor).
Behavioral Patterns¶
Omitted (not React-specific)¶
- Strategy via "pass a function/prop" and Chain of Responsibility pipelines are usually plain TypeScript; see
references/snippets/typescript.md.
Contents¶
- Observer (useSyncExternalStore)
- Command (useReducer-managed history)
- State (useReducer state machine)
Observer (useSyncExternalStore + adapter)¶
import * as React from 'react';
type Listener = () => void;
type Store<T> = { getSnapshot: () => T; subscribe: (l: Listener) => () => void };
// Factory Method: create a minimal external store (Atom).
export type Atom<T> = {
get: () => T;
set: (next: T) => void;
subscribe: (l: Listener) => () => void;
};
export const createAtom = <T,>(initial: T): Atom<T> => {
let value = initial;
const listeners = new Set<Listener>();
return {
get: () => value,
set: (next) => {
value = next;
listeners.forEach((l) => l());
},
subscribe: (l) => {
listeners.add(l);
return () => listeners.delete(l);
},
};
};
// Adapter: Atom -> Store interface expected by useSyncExternalStore.
export const toStore = <T,>(atom: Atom<T>): Store<T> => ({
getSnapshot: atom.get,
subscribe: atom.subscribe,
});
export const useStore = <T,>(store: Store<T>) =>
React.useSyncExternalStore(store.subscribe, store.getSnapshot);
Command (undoable UI actions with useReducer)¶
Represent user actions as objects; keep undo/redo in one place.
import * as React from 'react';
type Command = { execute: () => void; undo: () => void };
// Decorator: wrap a command with a policy (telemetry, confirmations, etc.).
const withTelemetry = (name: string, inner: Command): Command => ({
execute: () => {
console.log('run', name);
inner.execute();
},
undo: () => {
console.log('undo', name);
inner.undo();
},
});
type History = {
past: Command[];
present: Command | null;
future: Command[];
};
type Action =
| { type: 'push'; command: Command }
| { type: 'undo' }
| { type: 'redo' };
// Strategy registry: one state-transition strategy per action type.
type Transitions = {
[K in Action['type']]: (state: History, action: Extract<Action, { type: K }>) => History;
};
const transitions = {
push: (state, action) => ({
past: [...state.past, ...(state.present ? [state.present] : [])],
present: action.command,
future: [],
}),
undo: (state) => {
const current = state.present;
const prev = state.past[state.past.length - 1] ?? null;
return {
past: state.past.slice(0, -1),
present: prev,
future: current ? [current, ...state.future] : state.future,
};
},
redo: (state) => {
const next = state.future[0] ?? null;
if (!next) {
return state;
}
return {
past: [...state.past, ...(state.present ? [state.present] : [])],
present: next,
future: state.future.slice(1),
};
},
} satisfies Transitions;
const reducer = (state: History, action: Action): History =>
// TS needs a small cast because `action.type` is a union key; the strategies remain type-checked above.
(transitions[action.type] as (s: History, a: Action) => History)(state, action);
export const useCommandHistory = () => {
const [state, dispatch] = React.useReducer(reducer, { past: [], present: null, future: [] });
const decorate = React.useCallback((command: Command) => withTelemetry('ui', command), []);
const run = React.useCallback(
(command: Command) => {
const decorated = decorate(command);
decorated.execute();
dispatch({ type: 'push', command: decorated });
},
[decorate, dispatch],
);
const undo = React.useCallback(() => {
state.present?.undo();
dispatch({ type: 'undo' });
}, [state.present, dispatch]);
const redo = React.useCallback(() => {
state.future[0]?.execute();
dispatch({ type: 'redo' });
}, [state.future, dispatch]);
return {
run,
undo,
redo,
canUndo: state.present !== null || state.past.length > 0,
canRedo: state.future.length > 0,
};
};
State (state machine via useReducer)¶
You can keep state-specific behavior explicit instead of scattering if/else across components.
import * as React from 'react';
type State =
| { kind: 'idle' }
| { kind: 'loading' }
| { kind: 'error'; message: string };
type Action = { type: 'load' } | { type: 'ok' } | { type: 'fail'; message: string };
const stateReducer = (state: State, action: Action): State => {
switch (state.kind) {
case 'idle':
if (action.type === 'load') {
return { kind: 'loading' };
}
return state;
case 'loading':
if (action.type === 'ok') {
return { kind: 'idle' };
}
if (action.type === 'fail') {
return { kind: 'error', message: action.message };
}
return state;
case 'error':
if (action.type === 'load') {
return { kind: 'loading' };
}
return state;
}
};
export const Example = () => {
const [state, dispatch] = React.useReducer(stateReducer, { kind: 'idle' });
return (
<div>
<button onClick={() => dispatch({ type: 'load' })}>Load</button>
<pre>{JSON.stringify(state)}</pre>
</div>
);
};