How do you optimize working with context?
1) Split context into small pieces
Do not put "everything at once" into one Provider. Split by domain - then when one value changes, only its consumers re-render.
<UserProvider value={user}>
<ThemeProvider value={theme}>
<I18nProvider value={i18n}>
<App />
</I18nProvider>
</ThemeProvider>
</UserProvider>2) Memoize value
The main rule: the value reference should change only when its data changes.
function ThemeProvider({ children }) {
const [theme, setTheme] = useState<'light'|'dark'>('light');
const value = useMemo(() => ({ theme, setTheme }), [theme]); // good
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
}An anti-example (every render creates a new object -> all consumers get triggered):
<ThemeContext.Provider value={{ theme, setTheme }}> {/* bad */}3) Separate "changes often" from "changes rarely"
Keep fast-changing data separate from stable data.
const CounterValueContext = createContext<number>(0);
const CounterActionsContext = createContext<{ inc: () => void }>({ inc: () => {} });
function CounterProvider({ children }) {
const [count, setCount] = useState(0);
const inc = useCallback(() => setCount(c => c + 1), []);
return (
<CounterActionsContext.Provider value={{ inc }}>
<CounterValueContext.Provider value={count}>
{children}
</CounterValueContext.Provider>
</CounterActionsContext.Provider>
);
}Now, changing count does not change the reference to actions, and consumers of the actions do not re-render.
4) Keep the Provider as close to consumers as possible
Place the Provider around the part of the tree where the context is actually needed - fewer "subscribers" means fewer re-renders.
5) Use selectors (targeted reads)
Out of the box, React re-renders all consumers, but you can narrow updates with context-selector libraries:
use-context-selector- Zustand/Jotai/Redux Toolkit with
useSelector
Example with use-context-selector:
import { createContext, useContextSelector } from 'use-context-selector';
const CartContext = createContext<{items: Item[]; total: number}>({items:[], total:0});
function TotalPrice() {
const total = useContextSelector(CartContext, v => v.total); // re-renders only when total changes
return <span>{total}</span>;
}6) Do not pass "freshly created" objects/arrays
If you need to hand out derived data - memoize it on the consumer side:
function ProductsList() {
const { items } = useContext(ProductsContext);
const visible = useMemo(() => items.filter(p => p.visible), [items]);
return <List items={visible} />;
}7) For global mutable state - useReducer
dispatch is stable by reference -> convenient to pass through context.
const StoreContext = createContext<{state: State; dispatch: React.Dispatch<Action>}>(null!);
function StoreProvider({ children }) {
const [state, dispatch] = useReducer(reducer, initial);
const value = useMemo(() => ({ state, dispatch }), [state]); // dispatch is stable
return <StoreContext.Provider value={value}>{children}</StoreContext.Provider>;
}8) External stores: useSyncExternalStore
If you need a thin subscription without re-rendering the whole context, move the data to an external store and subscribe:
const subscribe = (listener: () => void) => store.subscribe(listener);
const getSnapshot = () => store.getState().pieceYouNeed;
const usePiece = () => useSyncExternalStore(subscribe, getSnapshot);This gives "selective" updates out of the box.
9) Stabilize functions in value
If value contains functions - wrap them in useCallback so the reference does not change for no reason.
const actions = useMemo(() => ({ save, remove }), [save, remove]); // where save/remove are useCallback10) Context is for "config", not for "counters"
The less often a context changes, the better. Keep frequently updated pieces of state local (useState in components) or in an external store; context is for configuration, theming, permissions, locale, DI services, and so on.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.