Generic context provider pattern.
An example on how to implement context provider wrapper and reducer to interact with.
Wrap the subtree that needs access to this context:
Wires the reducer, dispatcher, and memoised context value together.
<ExampleContextProvider>
<YourFeatureRoot />
</ExampleContextProvider>
Consumers call `useExampleContext()` (from ExampleContext.ts) to access
state and dispatch.
// ─────────────────────────────────────────────────────────────────────────────
// ExampleContextProvider.tsx
//
// ─────────────────────────────────────────────────────────────────────────────
import React, { JSX, useCallback, useMemo, useReducer } from 'react';
import {
ExampleContext,
ExampleContextInterface,
ExampleDispatcher,
startingExampleState,
} from './ExampleContext';
import { ExampleActions } from './ExampleContext.actions';
import { exampleReducer } from './ExampleContext.reducer';
export type ExampleContextProviderProps = {
children: React.ReactNode;
};
export const ExampleContextProvider = ({
children,
}: Readonly<ExampleContextProviderProps>): JSX.Element => {
const [state, _dispatch] = useReducer(exampleReducer, startingExampleState);
const dispatch: ExampleDispatcher = useCallback((type, ...payload) => {
_dispatch({ type, payload: payload[0] } as ExampleActions);
}, []);
const value: ExampleContextInterface = useMemo(() => [state, dispatch], [state, dispatch]);
return <ExampleContext.Provider value={value}>{children}</ExampleContext.Provider>;
};
ExampleContext.actions.ts
Defines the discriminated union of all actions the context supports.
USAGE:
1. Add an entry to `ExampleActionsMap` for every action your context needs.
- The key is the action type string (e.g. 'setItem').
- The value is the payload type, or `undefined` if no payload is needed.
2. The `ExampleActions` union is derived automatically — do not edit it.
// TODO: Replace with real domain types.
export type ExampleItem = {
id: string;
name: string;
};
/**
* Map of every supported action type → its payload shape.
* Add, rename, or remove entries here to extend the context.
*/
export type ExampleActionsMap = {
/** Select / set the active item. */
setItem: ExampleItem;
/** Clear the active item. */
clearItem: undefined;
/** Toggle a boolean flag. */
toggleFlag: { flag: boolean };
};
/**
* Discriminated union of all context actions.
* Consumed by the reducer and the typed dispatcher.
*/
export type ExampleActions = {
[Key in keyof ExampleActionsMap]: {
type: Key;
payload: ExampleActionsMap[Key];
};
}[keyof ExampleActionsMap];
ExampleContext.reducer.ts
Pure reducer function that drives all state transitions for ExampleContext.
RULES:
• Never mutate "state" — always spread into a new object.
• Every action type in "ExampleActionsMap" must have a matching "case".
• Complex derivations belong in a separate util and should be unit-tested independently.
import { ExampleState } from './ExampleContext';
import { ExampleActions } from './ExampleContext.actions';
export const exampleReducer = (state: ExampleState, action: ExampleActions): ExampleState => {
switch (action.type) {
case 'setItem':
return {
...state,
selectedItem: action.payload,
};
case 'clearItem':
return {
...state,
selectedItem: null,
};
case 'toggleFlag':
return {
...state,
flag: action.payload.flag,
};
// TypeScript will error here if a new action type is added to
// ExampleActionsMap but not handled in this switch.
default: {
const _exhaustiveCheck: never = action;
return _exhaustiveCheck;
}
}
};
// ─────────────────────────────────────────────────────────────────────────────
// ExampleContext.ts
//
// Declares the React context, its state shape, the typed dispatcher, and the
// initial state.
//
// USAGE:
// 1. Extend `ExampleState` with the fields your feature needs.
// 2. Update `startingExampleState` to provide safe initial values.
// 3. Import `useExampleContext` in consumer components instead of calling
// `useContext(ExampleContext)` directly.
// ─────────────────────────────────────────────────────────────────────────────
import { createContext, useContext } from 'react';
import { ExampleActions, ExampleActionsMap, ExampleItem } from './ExampleContext.actions';
// ── State ────────────────────────────────────────────────────────────────────
/**
* Shape of the data held in the context.
* TODO: Replace with real domain fields.
*/
export type ExampleState = {
selectedItem: ExampleItem | null;
flag: boolean;
};
// ── Dispatcher ───────────────────────────────────────────────────────────────
/**
* Type-safe dispatcher.
* - For actions *with* a payload, both arguments are required.
* - For actions *without* a payload (`undefined`), the second argument is optional.
*
* Example:
* dispatch('setItem', { id: '1', name: 'Foo' });
* dispatch('clearItem');
*/
export type ExampleDispatcher = <
Type extends ExampleActions['type'],
Payload extends ExampleActionsMap[Type],
>(
type: Type,
...payload: Payload extends undefined ? [undefined?] : [Payload]
) => void;
// ── Context interface ─────────────────────────────────────────────────────────
/**
* The context value is a `[state, dispatch]` tuple — same pattern as `useState`.
*/
export type ExampleContextInterface = readonly [ExampleState, ExampleDispatcher];
// ── Initial state ─────────────────────────────────────────────────────────────
export const startingExampleState: ExampleState = {
selectedItem: null,
flag: false,
};
// ── Context instance ──────────────────────────────────────────────────────────
export const ExampleContext = createContext<ExampleContextInterface>([
startingExampleState,
() => {},
]);
// ── Consumer hook ─────────────────────────────────────────────────────────────
/**
* Convenience hook — call this in consumer components.
* Throws a descriptive error when used outside the provider.
*
* @example
* const [{ selectedItem }, dispatch] = useExampleContext();
*/
export const useExampleContext = (): ExampleContextInterface => {
const context = useContext(ExampleContext);
if (!context) {
throw new Error('useExampleContext must be used within an ExampleContextProvider');
}
return context;
};