State Management
March 25, 20252 min read...
State ManagementMarch 25, 20252 min read

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.

React Context + useReducer: Scalable State Without Redux

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.