React Hooks — useContext & useReducer
useContext
useContext provides a way to pass data through the component tree without prop drilling.
// 1. Create context
const ThemeContext = createContext('light');
// 2. Provide it
function App() {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<Page />
</ThemeContext.Provider>
);
}
// 3. Consume it (any depth)
function Button() {
const { theme } = useContext(ThemeContext);
return <button className={theme}>Click</button>;
}
Performance Problem
All consumers re-render when the provider value changes, even if they use only part of the value.
// ❌ Monolithic context: changing any property re-renders everything
const AppContext = createContext();
const [state, setState] = useState({ user, cart, ui, filters });
// ✅ Split by change frequency
const UserContext = createContext(); // Changes rarely (login/logout)
const CartContext = createContext(); // Changes on add/remove
const UIContext = createContext(); // Changes often (modals, tooltips)
Memoize Provider Value
// ❌ New object on every render → all consumers re-render
<AuthContext.Provider value={{ user, login, logout }}>
// ✅ Stable reference
const value = useMemo(() => ({ user, login, logout }), [user]);
<AuthContext.Provider value={value}>
When NOT to Use Context
- Frequently changing values (use Zustand/Redux instead)
- Props only needed 1–2 levels down (just pass as props)
- Component-specific state
useReducer
useReducer manages complex state logic via a reducer function (state, action) => newState. Best when state has multiple sub-values or complex transitions.
const initialState = { count: 0, step: 1, history: [] };
function counterReducer(state, action) {
switch (action.type) {
case 'increment':
return {
...state,
count: state.count + state.step,
history: [...state.history, state.count]
};
case 'decrement':
return { ...state, count: state.count - state.step };
case 'setStep':
return { ...state, step: action.payload };
case 'reset':
return initialState;
default:
return state;
}
}
function Counter() {
const [state, dispatch] = useReducer(counterReducer, initialState);
return (
<>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
<button onClick={() => dispatch({ type: 'setStep', payload: 5 })}>Step: 5</button>
</>
);
}
Context + useReducer (Global State Pattern)
Separate state and dispatch into two contexts to minimize re-renders:
const StateContext = createContext();
const DispatchContext = createContext();
function AppProvider({ children }) {
const [state, dispatch] = useReducer(appReducer, initialState);
return (
<DispatchContext.Provider value={dispatch}>
<StateContext.Provider value={state}>
{children}
</StateContext.Provider>
</DispatchContext.Provider>
);
}
// Components that only dispatch won't re-render when state changes
function AddButton() {
const dispatch = useContext(DispatchContext);
return <button onClick={() => dispatch({ type: 'add' })}>Add</button>;
}
useState vs useReducer
Use useState | Use useReducer |
|---|---|
| 1–2 independent values | 4+ related values |
| Simple updates | Multiple update patterns |
| No inter-field logic | Next state depends on multiple current values |
| Independent form fields | Shopping cart (items + total + tax) |
Reducers are Pure Functions — Easy to Test
test('increment increases count by step', () => {
const state = { count: 5, step: 2 };
const next = counterReducer(state, { type: 'increment' });
expect(next.count).toBe(7);
});
Real-World: E-commerce Cart
function cartReducer(state, action) {
switch (action.type) {
case 'ADD_ITEM':
const existing = state.items.find(i => i.id === action.item.id);
const items = existing
? state.items.map(i => i.id === action.item.id ? { ...i, qty: i.qty + 1 } : i)
: [...state.items, { ...action.item, qty: 1 }];
return {
items,
total: items.reduce((sum, i) => sum + i.price * i.qty, 0),
tax: items.reduce((sum, i) => sum + i.price * i.qty, 0) * 0.1
};
case 'REMOVE_ITEM':
const filtered = state.items.filter(i => i.id !== action.id);
return { items: filtered, total: /* ... */ };
default:
return state;
}
}
// All derived values (total, tax) stay in sync automatically
Content from Frontend-Master-Prep-Series — 03-react