Skip to main content

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 useStateUse useReducer
1–2 independent values4+ related values
Simple updatesMultiple update patterns
No inter-field logicNext state depends on multiple current values
Independent form fieldsShopping 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-Series03-react