Context + useReducer
Combining useReducer with Context gives you a lightweight state management system without any external library.
Implementation
tsx
// types
type State = { count: number; theme: 'light' | 'dark' };
type Action =
| { type: 'increment' }
| { type: 'toggle-theme' };
// reducer
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'increment': return { ...state, count: state.count + 1 };
case 'toggle-theme': return { ...state, theme: state.theme === 'light' ? 'dark' : 'light' };
}
}
// context
const StateCtx = createContext<State | null>(null);
const DispatchCtx = createContext<React.Dispatch<Action> | null>(null);
// provider
export function AppProvider({ children }: { children: React.ReactNode }) {
const [state, dispatch] = useReducer(reducer, { count: 0, theme: 'light' });
return (
<StateCtx.Provider value={state}>
<DispatchCtx.Provider value={dispatch}>
{children}
</DispatchCtx.Provider>
</StateCtx.Provider>
);
}
// hooks
export const useAppState = () => useContext(StateCtx)!;
export const useAppDispatch = () => useContext(DispatchCtx)!;Usage
tsx
function Counter() {
const { count } = useAppState();
const dispatch = useAppDispatch();
return <button onClick={() => dispatch({ type: 'increment' })}>{count}</button>;
}When to use vs a library
| Scenario | Context + useReducer | Zustand / Redux |
|---|---|---|
| Small app or single feature | Yes | Overkill |
| Complex update logic | Yes | Yes |
| High-frequency updates | No - re-renders all consumers | Yes - selectors |
| DevTools needed | No | Yes |
| Team convention | Depends | Preferred |
Splitting state and dispatch into two contexts is important - components that only dispatch won't re-render when state changes.
updated
did you know it?