Combining Context and useReducer creates a Redux-like global state solution without external dependencies. This pattern separates state and dispatch to optimize performance.
Complete implementation:
1import {2 createContext,3 useContext,4 useReducer,5 ReactNode,6} from "react";78// Types9type Todo = { id: string; text: string; done: boolean };1011type State = {12 user: { name: string; email: string } | null;13 theme: "light" | "dark";14 todos: Todo[];15};1617type Action =18 | { type: "SET_USER"; payload: State["user"] }19 | { type: "TOGGLE_THEME" }20 | { type: "ADD_TODO"; payload: string }21 | { type: "TOGGLE_TODO"; payload: string }22 | { type: "DELETE_TODO"; payload: string };2324// Reducer — pure function, easy to test25function reducer(state: State, action: Action): State {26 switch (action.type) {27 case "SET_USER":28 return { ...state, user: action.payload };29 case "TOGGLE_THEME":30 return {31 ...state,32 theme: state.theme === "light" ? "dark" : "light",33 };34 case "ADD_TODO":35 return {36 ...state,37 todos: [38 ...state.todos,39 { id: crypto.randomUUID(), text: action.payload, done: false },40 ],41 };42 case "TOGGLE_TODO":43 return {44 ...state,45 todos: state.todos.map(t =>46 t.id === action.payload ? { ...t, done: !t.done } : t47 ),48 };49 case "DELETE_TODO":50 return {51 ...state,52 todos: state.todos.filter(t => t.id !== action.payload),53 };54 default:55 return state;56 }57}5859// Separate contexts for state and dispatch (performance optimization)60const AppStateContext = createContext<State | null>(null);61const AppDispatchContext = createContext<React.Dispatch<Action> | null>(null);6263// Provider64export function AppProvider({ children }: { children: ReactNode }) {65 const [state, dispatch] = useReducer(reducer, {66 user: null,67 theme: "light" as const,68 todos: [],69 });7071 return (72 <AppStateContext.Provider value={state}>73 <AppDispatchContext.Provider value={dispatch}>74 {children}75 </AppDispatchContext.Provider>76 </AppStateContext.Provider>77 );78}7980// Custom hooks with error boundaries81export function useAppState() {82 const context = useContext(AppStateContext);83 if (!context) throw new Error("useAppState must be used within AppProvider");84 return context;85}8687export function useAppDispatch() {88 const context = useContext(AppDispatchContext);89 if (!context) throw new Error("useAppDispatch must be used within AppProvider");90 return context;91}9293// Usage in components94function TodoList() {95 const { todos } = useAppState();96 const dispatch = useAppDispatch();9798 return (99 <ul>100 {todos.map(todo => (101 <li key={todo.id}>102 <span103 onClick={() => dispatch({ type: "TOGGLE_TODO", payload: todo.id })}104 style={{ textDecoration: todo.done ? "line-through" : "none" }}105 >106 {todo.text}107 </span>108 <button onClick={() => dispatch({ type: "DELETE_TODO", payload: todo.id })}>109 Delete110 </button>111 </li>112 ))}113 </ul>114 );115}
Why split state and dispatch contexts:
dispatch is stable across renders (React guarantees this), so components that only use dispatch never re-render. Components using useAppState() only re-render when state changes.
Benefits: