Context Provider Boilerplate

1 day ago 2
ARTICLE AD BOX

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; };
Read Entire Article