- React
- State Architecture, Store Subscriptions, and Server Cache
- Context API at Scale: Splitting & Selector Patterns
Context API at Scale: Splitting & Selector Patterns
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';23interface CartState {4 items: string[];5 total: number;6}78type CartAction = { type: 'ADD'; item: string } | { type: 'CLEAR' };910// 1. Separate State Context and Dispatch Context11const CartStateContext = createContext<CartState | null>(null);12const CartDispatchContext = createContext<React.Dispatch<CartAction> | null>(null);1314function 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}2425export function CartProvider({ children }: { children: React.ReactNode }) {26 const [state, dispatch] = useReducer(cartReducer, { items: [], total: 0 });2728 return (29 <CartDispatchContext.Provider value={dispatch}>30 <CartStateContext.Provider value={state}>31 {children}32 </CartStateContext.Provider>33 </CartDispatchContext.Provider>34 );35}3637// 2. Custom hooks with null safety checks38export function useCartState() {39 const context = useContext(CartStateContext);40 if (!context) throw new Error('useCartState must be used within CartProvider');41 return context;42}4344export 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:
- Every single component calling
useContext(MyContext)will re-render. React.memoon the consuming component cannot stop this re-render. Context updates bypass memoization.- If
value={{ state, dispatch }}is passed as an inline object, any timestateupdates, even components that only needdispatchwill re-render!
The Split Context Solution
By splitting into CartStateContext and CartDispatchContext:
- Components that trigger actions (e.g.
<AddToCartButton />) consume onlyCartDispatchContext. - Because
dispatchfromuseReduceris 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 />) consumeCartStateContextand 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
What is the Split Context pattern?
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.
Does useMemo around Provider value solve all Context re-render problems?
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 should a team switch from Context API to Zustand or Redux?
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.
Why is useReducer dispatch guaranteed to be referentially stable?
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.
Can you use multiple nested Context Providers?
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.
