Split Context
Use smaller contexts like UserContext and ThemeContext instead of one big object.
Context API helps you share data across a component subtree without manually passing props through every level.
It is best for app-wide or subtree-wide values that many components need, such as auth state, theme, locale, feature flags, and dependency objects.
When deeply nested components need the same data, props have to pass through many middle components that may not use that data.
Context introduces a provider/consumer model.
createContext()useContext(): the modern way to read context in function componentsUse context for values that are:
Good examples:
import { createContext } from "react";
const UserContext = createContext(null);Always give a sensible default when possible. null is common because it makes missing providers easier to detect.
<UserContext.Provider value={data}> <App /></UserContext.Provider>The provider makes data available to every descendant component that reads this context.
<UserContext.Consumer>{(value) => <h1>{value}</h1>}</UserContext.Consumer>This is the legacy render-prop API. It still works, but in modern function components, useContext() is usually better.
useContext()import { useContext } from "react";
function Profile() { const user = useContext(UserContext);
return <h1>{user?.name}</h1>;}If the provider is missing, useContext() returns the default value from createContext().
Create context.
const ThemeContext = createContext("light");Provide value.
function App() { return ( <ThemeContext.Provider value="dark"> <Child /> </ThemeContext.Provider> );}Consume value.
function Child() { const theme = useContext(ThemeContext);
return <h1>{theme}</h1>;}This is the simplest flow:
useContext()class Child extends React.Component { static contextType = ThemeContext;
render() { return <h1>{this.context}</h1>; }}Class components are older style, but this API is still relevant when maintaining legacy code.
import { createContext, useMemo, useState } from "react";
const ThemeContext = createContext(null);
function App() { const [theme, setTheme] = useState("light");
const value = useMemo(() => ({ theme, setTheme }), [theme]);
return ( <ThemeContext.Provider value={value}> <Child /> </ThemeContext.Provider> );}This is the standard way to both get and set data through context:
theme is the shared valuesetTheme is the updater functionuseMemo() keeps the provider value reference stable when the data has not changed<UserContext.Provider value={user}> <ThemeContext.Provider value={theme}> <App /> </ThemeContext.Provider></UserContext.Provider>Use multiple contexts when the concerns are unrelated. This keeps updates smaller and easier to reason about.
Examples:
AuthContextThemeContextSettingsContext<AppContext.Provider value={{ user, theme, locale, notifications, cart, permissions }}>This creates a giant catch-all context. Every change can re-render many consumers, and the code becomes harder to maintain.
Pattern:
function App() { const [count, setCount] = useState(0);
const value = useMemo(() => ({ count, setCount }), [count]);
return ( <CountContext.Provider value={value}> <Child /> </CountContext.Provider> );}function Child() { const { count, setCount } = useContext(CountContext);
return ( <button onClick={() => setCount(count + 1)}>{count}</button> );}The child can both read and update the shared value, but the provider still owns the state.
const CountContext = createContext(null);
function CountProvider({ children }) { const [count, setCount] = useState(0);
const value = useMemo(() => ({ count, setCount }), [count]);
return <CountContext.Provider value={value}>{children}</CountContext.Provider>;}
function useCount() { const context = useContext(CountContext);
if (!context) { throw new Error("useCount must be used within CountProvider"); }
return context;}This custom hook gives you:
Each context object contains:
{ currentValue, Provider, Consumer}Important behavior:
Problem example:
value={{ user, theme }}If only theme changes, user consumers may still re-render.
That happens because React compares provider values by reference, not by deep inspection of object contents.
Split Context
Use smaller contexts like UserContext and ThemeContext instead of one big object.
Memoize Value
Use useMemo for stable provider value object references.
Avoid Large Objects
Smaller focused values reduce unnecessary consumer updates.
const value = useMemo(() => ({ user, setUser }), [user]);Additional optimization rules:
function Provider({ children }) { const [state, setState] = useState({ user, theme, locale });
return <AppContext.Provider value={{ state, setState }}>{children}</AppContext.Provider>;}This is hard to optimize because almost any update changes the object reference and can re-render many consumers.
function ThemeButton() { const { theme, setTheme } = useContext(ThemeContext);
return ( <button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>Current: {theme}</button> );}The provider owns the data. Consumers read the value and call the updater. That is the cleanest Context pattern for beginners.
| Feature | Props | Context |
|---|---|---|
| Scope | Local | Global for subtree |
| Control | Explicit | Implicit |
| Reusability | High | Moderate |
| Debugging | Easy | Harder |
Use props when the data only travels one or two levels. Use context when many descendants need the same value.
| Feature | Context | Redux |
|---|---|---|
| Complexity | Low | High |
| Performance | Medium | High |
| DevTools | Limited | Powerful |
| Use case | Small-medium apps | Large apps |
Context is not a full state management framework. If your state needs logging, time travel, complex async flows, or large shared updates, Redux Toolkit or Zustand may be better.
import { createContext, useReducer } from "react";
const StoreContext = createContext(null);
function reducer(state, action) { switch (action.type) { case "increment": return { count: state.count + 1 }; default: return state; }}This pattern is useful when updates are more complex than simple setters. It keeps the state transitions predictable.
function StoreProvider({ children }) { const [state, dispatch] = useReducer(reducer, { count: 0 });
return <StoreContext.Provider value={{ state, dispatch }}>{children}</StoreContext.Provider>;}In production, this wrapper keeps provider setup out of your app tree and makes the context reusable.
function useStore() { const context = useContext(StoreContext);
if (!context) { throw new Error("useStore must be used within StoreProvider"); }
return context;}This is a strong production pattern because it prevents silent failures when a provider is missing.
useMemoThis structure is common in production apps because each provider owns one concern.
Example layout:
function App() { return ( <AuthProvider> <ThemeProvider> <SettingsProvider> <Router /> </SettingsProvider> </ThemeProvider> </AuthProvider> );}Context = broadcast systemWhen the provider value changes, React broadcasts that change to the consuming subtree.
Fiber integration:
Each fiber tracks context dependenciesReact uses the provider value reference to decide which consuming components need to re-render.
That is why a new object literal like value={{ user }} can cause extra work even if the contents look the same.
Then consider:
In practice:
Before creating a context, ask:
If the answer is no, use props or local state instead.