Optimize context usage to avoid unnecessary re-renders with splitting and memoization.
Understand that every context consumer re-renders when any value in the context changes.
const AppContext = createContext<{ user: User | null; theme: string } | undefined>(undefined);
function AppProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [theme, setTheme] = useState("light");
// All consumers re-render if EITHER user or theme changes
return (
<AppContext.Provider value={{ user, theme, setUser, setTheme }}>
{children}
</AppContext.Provider>
);
}
Recognize that inline object literals create a new reference on every render, triggering all consumers.
// Bad: new object on every parent render — all consumers re-render unnecessarily
<MyContext.Provider value={{ user, setUser }}>
{children}
</MyContext.Provider>
// Good: stable reference when user hasn't changed
const value = useMemo(() => ({ user, setUser }), [user]);
<MyContext.Provider value={value}>{children}</MyContext.Provider>
Profile with React DevTools before optimizing to confirm which components actually re-render.
// Open React DevTools > Profiler tab
// Record an interaction, then inspect the flame chart
// Only optimize components that show unnecessary renders
Spot the problem by logging renders inside a context consumer.
function ThemeButton() {
const { theme } = useContext(AppContext)!;
console.log("ThemeButton rendered"); // fires on every user change too
return <button className={theme}>Click me</button>;
}
Understand that splitting contexts into smaller pieces is the primary fix for this problem.
// Instead of one big AppContext, use focused per-domain contexts:
// UserContext → only user data
// ThemeContext → only theme data
// Each consumer subscribes only to what it needs
Create separate contexts for independent pieces of state so updates are isolated.
const UserContext = createContext<{ user: User | null; setUser: (u: User | null) => void } | undefined>(undefined);
const ThemeContext = createContext<{ theme: string; setTheme: (t: string) => void } | undefined>(undefined);
function AppProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [theme, setTheme] = useState("light");
return (
<UserContext.Provider value={{ user, setUser }}>
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
</UserContext.Provider>
);
}
Confirm that user-only consumers no longer re-render on theme changes after the split.
function UserBadge() {
const { user } = useContext(UserContext)!;
// Only re-renders when user changes — theme updates are ignored
return <span>{user?.name}</span>;
}
Let each context update independently so unrelated parts of the tree stay stable.
function ThemeToggle() {
const { theme, setTheme } = useContext(ThemeContext)!;
// Changing theme does NOT re-render UserBadge above
return <button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>{theme}</button>;
}
Split read and write into separate contexts so read-only consumers never re-render from setter changes.
const UserStateContext = createContext<User | null>(null); // just the value
const UserDispatchContext = createContext<(u: User | null) => void>(() => {}); // just the setter
// Components that only display user data never re-render when setUser reference changes
function UserDisplay() {
const user = useContext(UserStateContext);
return <p>{user?.name}</p>;
}
Subscribe only to the contexts a component needs by consuming each context separately.
function AdminPanel() {
const { user } = useContext(UserContext)!; // user updates trigger this
// ThemeContext NOT consumed here — theme changes are irrelevant
return user?.role === "admin" ? <div>Admin tools</div> : null;
}
Wrap the context value object in useMemo so a new reference is only created when dependencies change.
function UserProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const userValue = useMemo(
() => ({ user, setUser }),
[user] // only creates a new object when user changes
);
return (
<UserContext.Provider value={userValue}>
{children}
</UserContext.Provider>
);
}
List every value used from the context object in the dependency array to avoid stale closures.
const value = useMemo(
() => ({ user, settings, updateUser }),
[user, settings, updateUser] // include all three dependencies
);
Combine useMemo with stable function references so functions don't invalidate the memo.
const updateUser = useCallback((newUser: User) => {
setUser(newUser);
}, []); // stable reference — empty deps because setUser is always stable
const value = useMemo(() => ({ user, updateUser }), [user, updateUser]);
Verify that memoization is working by checking that consumers don't re-render on unrelated parent renders.
// Parent re-renders due to its own state, but context value stays the same
// Consumers should NOT re-render — confirm in React DevTools Profiler
function ParentWithOwnState() {
const [tick, setTick] = useState(0); // this state is unrelated to context
return <button onClick={() => setTick(t => t + 1)}>Tick: {tick}</button>;
}
Avoid memoizing the initial value inline — always use useMemo inside the provider component.
// Wrong: initial value object is only created once but is a constant, not reactive
<MyContext.Provider value={{ user: null, setUser: () => {} }}>
// Right: useMemo inside the provider component reacts to state changes
const value = useMemo(() => ({ user, setUser }), [user]);
<MyContext.Provider value={value}>
Wrap context consumer components in React.memo so they skip re-renders when props are unchanged.
const UserDisplay = React.memo(function UserDisplay() {
const { user } = useContext(UserContext)!;
// Skips re-renders when parent state changes but context value is stable
return <div>{user?.name}</div>;
});
Combine React.memo with a memoized context value for the best re-render reduction.
// Provider memoizes the value object
const value = useMemo(() => ({ user, setUser }), [user]);
// Consumer is wrapped in React.memo
const UserBadge = React.memo(() => {
const { user } = useContext(UserContext)!;
return <span>{user?.name}</span>;
});
// Only re-renders when the user object actually changes
Understand that React.memo still re-renders when the context value itself changes.
// React.memo protects against prop changes, not context changes
// If useContext returns a new reference, the component WILL re-render
// Combine with useMemo on the provider value to control this
Pass a custom comparison function to React.memo for fine-grained prop-level control.
const UserDisplay = React.memo(
({ label }: { label: string }) => {
const { user } = useContext(UserContext)!;
return <p>{label}: {user?.name}</p>;
},
(prevProps, nextProps) => prevProps.label === nextProps.label // only re-render if label changes
);
Extract the context-consuming logic into a child component so React.memo can protect the expensive UI.
// Expensive chart component is memoized; only re-renders when chartData changes
const Chart = React.memo(({ chartData }: { chartData: number[] }) => {
return <canvas>{/* expensive render */}</canvas>;
});
function ChartContainer() {
const { chartData } = useContext(DashboardContext)!;
return <Chart chartData={chartData} />; // passes stable reference when memoized
}
Wrap context callbacks in useCallback so their references stay stable across parent renders.
function UserProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const updateUser = useCallback((newUser: User) => {
setUser(newUser); // setUser is stable, so deps array is empty
}, []);
const value = useMemo(() => ({ user, updateUser }), [user, updateUser]);
return (
<UserContext.Provider value={value}>{children}</UserContext.Provider>
);
}
Understand that functions created inline on every render invalidate the memoized context object.
// Bad: inline function creates new reference every render
const value = useMemo(() => ({
user,
updateUser: (u: User) => setUser(u), // new reference each render
}), [user]); // updateUser changes every render, memo is useless
// Good: useCallback gives updateUser a stable reference
const updateUser = useCallback((u: User) => setUser(u), []);
const value = useMemo(() => ({ user, updateUser }), [user, updateUser]);
Use useReducer dispatch instead of multiple callbacks — dispatch is always stable, no useCallback needed.
const [state, dispatch] = useReducer(userReducer, initialState);
// dispatch never changes reference — safe to include in memoized context value
const value = useMemo(() => ({ state, dispatch }), [state]);
Compose multiple stable callbacks into a single memoized actions object for cleaner context values.
const actions = useMemo(() => ({
login: (credentials: Credentials) => dispatch({ type: "LOGIN", payload: credentials }),
logout: () => dispatch({ type: "LOGOUT" }),
updateProfile: (data: Partial<User>) => dispatch({ type: "UPDATE", payload: data }),
}), [dispatch]); // dispatch is stable, so actions object is stable
Prefer useReducer over useState in context providers to eliminate most useCallback needs.
// useState approach: need useCallback for every mutator
const [user, setUser] = useState(null);
const updateName = useCallback((name) => setUser(u => ({ ...u!, name })), []);
// useReducer approach: dispatch is always stable — no useCallback needed
const [state, dispatch] = useReducer(reducer, { user: null });
Every component subscribed to a context re-renders whenever any value in that context changes, even if the component only uses an unrelated field. Split your context into separate providers per concern so updates to one domain don't trigger consumers of another.
Wrap the value object in useMemo with its dependencies: const value = useMemo(() => ({ user, setUser }), [user]). Without this, a new object reference is created on every parent render, causing all consumers to re-render even when the data hasn't changed.
Wrap a consumer in React.memo when it receives context values as props passed down from a parent rather than via useContext directly — this lets React bail out of re-rendering if the specific props it receives haven't changed. Components using useContext directly still re-render on context changes regardless of React.memo.
Functions defined inline in a component recreate on every render, making them new references that invalidate the memoized context object. Wrap each function with useCallback and declare its dependencies so the reference stays stable between renders.
There's no hard limit — providers are cheap and multiple providers composed in a tree is the recommended pattern. Combining unrelated state into one context trades boilerplate reduction for broader re-render scope, which is usually the wrong tradeoff for frequently-updating values.