React Context + useReducer: Scalable State Without Redux
Learn to combine React Context with useReducer to create a scalable state management solution for small-to-medium apps – no external libraries needed. Includes TypeScript, performance optimizations, and testing.

Why Context + useReducer?
useReducer manages complex state logic. Context shares that state across the component tree. Together they replace Redux for many apps – without the boilerplate.
Implementation
// store/todoReducer.js
export const initialState = { todos: [], filter: 'all' };
export function todoReducer(state, action) {
switch (action.type) {
case ‘ADD_TODO’: return { …state, todos: […state.todos, action.payload] };
case ‘TOGGLE_TODO’: return { …state, todos: state.todos.map(t => t.id === action.id ? { …t, done: !t.done } : t) };
default: return state;
}
}
// context/TodoContext.jsx
const TodoContext = createContext();
export function TodoProvider({ children }) {
const [state, dispatch] = useReducer(todoReducer, initialState);
return <TodoContext.Provider value={{ state, dispatch }}>{children}</TodoContext.Provider>;
}
export function useTodo() {
const context = useContext(TodoContext);
if (!context) throw new Error(‘useTodo must be used within TodoProvider’);
return context;
}
Usage in Components
function AddTodo() {
const { dispatch } = useTodo();
const [text, setText] = useState(‘’);
return (
<form onSubmit={(e) => { e.preventDefault(); dispatch({ type: ‘ADD_TODO’, payload: { id: Date.now(), text, done: false } }); setText(‘’); }}>
<input value={text} onChange={e => setText(e.target.value)} />
</form>
);
}
function TodoList() {
const { state } = useTodo();
return state.todos.map(todo => <div key={todo.id}>{todo.text}</div>);
}
Performance Optimization
The above pattern causes all consumers to re-render on any state change. Fix with selectors:
function useTodoSelector(selector) {
const { state } = useTodo();
return selector(state);
}
// Then: const todos = useTodoSelector(state => state.todos);
Common Mistakes
- Putting the provider too high (re-renders entire app)
- Not memoizing dispatch actions
- Mutating state directly in reducer
Interview Q&A
Q: When should you switch from Context+Reducer to Redux? When you have frequent updates (many state changes per second), deeply nested selectors, or need devtools time-travel debugging.
Conclusion
For small-to-medium apps, Context + useReducer is simpler and uses zero dependencies. Add selectors for performance.
Comments
Join the conversation — sign in to leave a comment.