React Curriculum

Context API at Scale: Splitting & Selector Patterns

Medium100% Free~30 mins#react#context#state-management#performance#selectors#re-renders
Interview Answer (30-Second Summary)

React Context notifies every subscribing component whenever its value reference changes, regardless of whether the component uses the modified property. At scale, mitigate this by splitting state and dispatch into separate contexts, colocating providers, or using external stores with fine-grained selectors.

Key Learning Objectives
✓

Explain the fundamental performance limitation of React Context: lack of fine-grained selectors.

✓

Implement the split context pattern separating mutable state from stable dispatch functions.

✓

Memoize Context provider value objects to prevent re-rendering consumers when provider parents re-render.

✓

Recognize when an application has outgrown Context API and requires an external store like Zustand.

The Interview Problem & Context

Demonstrate how combining frequently changing state with dispatch functions in a single Context creates unnecessary re-render cascades, and refactor it into an optimized Split Context architecture.

1import React, { createContext, useContext, useReducer, useMemo } from 'react';
2
3interface CartState {
4 items: string[];
5 total: number;
6}
7
8type CartAction = { type: 'ADD'; item: string } | { type: 'CLEAR' };
9
10// 1. Separate State Context and Dispatch Context
11const CartStateContext = createContext<CartState | null>(null);
12const CartDispatchContext = createContext<React.Dispatch<CartAction> | null>(null);
13
14function cartReducer(state: CartState, action: CartAction): CartState {
15 switch (action.type) {
16 case 'ADD':
17 return { items: [...state.items, action.item], total: state.total + 10 };
18 case 'CLEAR':
19 return { items: [], total: 0 };
20 default:
21 return state;
22 }
23}
24
25export function CartProvider({ children }: { children: React.ReactNode }) {
26 const [state, dispatch] = useReducer(cartReducer, { items: [], total: 0 });
27
28 return (
29 <CartDispatchContext.Provider value={dispatch}>
30 <CartStateContext.Provider value={state}>
31 {children}
32 </CartStateContext.Provider>
33 </CartDispatchContext.Provider>
34 );
35}
36
37// 2. Custom hooks with null safety checks
38export function useCartState() {
39 const context = useContext(CartStateContext);
40 if (!context) throw new Error('useCartState must be used within CartProvider');
41 return context;
42}
43
44export function useCartDispatch() {
45 const context = useContext(CartDispatchContext);
46 if (!context) throw new Error('useCartDispatch must be used within CartProvider');
47 return context;
48}
Deep Architectural Explanation

The React Context Re-render Problem

When a Context Provider's value changes according to Object.is comparison:

  1. Every single component calling useContext(MyContext) will re-render.
  2. React.memo on the consuming component cannot stop this re-render. Context updates bypass memoization.
  3. If value={{ state, dispatch }} is passed as an inline object, any time state updates, even components that only need dispatch will re-render!

The Split Context Solution

By splitting into CartStateContext and CartDispatchContext:

  • Components that trigger actions (e.g. <AddToCartButton />) consume only CartDispatchContext.
  • Because dispatch from useReducer is referentially stable for the lifetime of the component, <AddToCartButton /> never re-renders when items are added to the cart!
  • Only components displaying cart contents (<CartSummary />) consume CartStateContext and re-render.
Common Interview Traps & Pitfalls
✕

Passing an un-memoized object literal like `value={{ user, theme }}` directly to Provider value.

✕

Putting high-frequency updates (like mouse coordinates, scroll positions, or timer ticks) into Context.

✕

Using a single mega-context for the entire application instead of domain-scoped providers.

Senior Interviewer Follow-Up Probes
?

How does Zustand solve this without triggering re-renders in unselected properties?

?

Why has React not added a native selector API (e.g., useContextSelector) to core React?

?

What is useSyncExternalStore and how do external stores use it to subscribe to sub-slices of state?

Frequently Asked Interview Questions
Why does Context re-render consumers even if the consumed property did not change?

React Context subscribes the entire component to the Provider value reference. React does not diff sub-properties of the context object; if Object.is(oldValue, newValue) is false, all consumers are scheduled to re-render.

Separating state and updater functions into two distinct Contexts (e.g., ThemeStateContext and ThemeDispatchContext). Components that only dispatch actions never re-render when state changes because the dispatch reference never changes.

It prevents consumers from re-rendering when the Provider component re-renders due to unrelated parent updates. However, whenever the memoized state actually changes, all consumers will still re-render.

When state has high-frequency updates, when multiple components consume fine-grained sub-slices of state and suffer performance degradation, or when complex middleware (logging, persistence, devtools) is required.

React guarantees by design that the dispatch function identity returned by useReducer and the updater returned by useState remain constant for the lifetime of the component instance across all renders.

Yes, composing nested providers is standard practice (e.g., AuthProvider > ThemeProvider > CartProvider). It scopes concerns cleanly, though deep provider pyramids can be flattened with utility composer functions.