Skip to content

Context API

Context API lets you share data with a group of components without manually passing props down through every single level.

It works best for values that the whole app or a big part of it needs, like login info, theme, language, feature flags, and shared tool objects.

When a component deep inside your app needs some data, that data has to pass through every component in between, even if those components do not use it themselves.

graph LR APP["App"] --> A["A"] A --> B["B"] B --> C["C"] C --> D["D needs data"]
  • Too much repeated code.
  • Hard to maintain.
  • Components get tightly connected to each other.
  • Components in the middle have to accept and pass forward props they don’t even use.

Context gives you a provider and consumer setup.

graph TD P["Provider stores data"] --> T["Data available to subtree"] T --> C["Consumer reads data directly"]
  • Context: the shared channel you create using createContext()
  • Provider: the component that gives a value to all components below it
  • Consumer: a component that reads the context value
  • useContext(): the modern way to read context inside function components
  • Value: the actual data you pass into the provider
  • Subtree: all the components that sit below a provider
  • Re-render: when React runs a component again because the context value it depends on has changed

Use context for data that:

  • Many components in the same part of the app need
  • Doesn’t change too often
  • Makes more sense as shared settings than as local state inside one component

Good examples:

  • Login or session info
  • Theme (light/dark mode)
  • Language or locale settings
  • UI settings like whether the sidebar is open or closed
  • Shared tool objects like API clients
  • When data only needs to go one or two levels down
  • When data changes very often, like mouse position, animation frames, or text being typed
  • When you have a large app state that needs proper devtools, saving to storage, or complex update logic
  • When the data belongs to just one component or a small group of components
import { createContext } from "react";
const UserContext = createContext(null);

Try to always give a sensible default value. null is common because it makes it easy to notice if you forgot to add a provider.

<UserContext.Provider value={data}>
<App />
</UserContext.Provider>

The provider makes data available to every component below it that wants to read this context.

<UserContext.Consumer>{(value) => <h1>{value}</h1>}</UserContext.Consumer>

This is the older way of reading context. It still works, but in modern function components, useContext() is usually a better choice.

import { useContext } from "react";
function Profile() {
const user = useContext(UserContext);
return <h1>{user?.name}</h1>;
}

If there is no provider above this component, useContext() will give back the default value you set in createContext().

  1. Create context.

    const ThemeContext = createContext("light");
  2. Provide value.

    function App() {
    return (
    <ThemeContext.Provider value="dark">
    <Child />
    </ThemeContext.Provider>
    );
    }
  3. Consume value.

    function Child() {
    const theme = useContext(ThemeContext);
    return <h1>{theme}</h1>;
    }

This is the simplest flow:

  • Create the context
  • Wrap your components inside a provider
  • Read the value using useContext()
class Child extends React.Component {
static contextType = ThemeContext;
render() {
return <h1>{this.context}</h1>;
}
}

Class components are the older style of writing React, but this way of using context is still useful if you are working on older 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 common way to both read and update data through context:

  • theme is the shared value
  • setTheme is the function used to change it
  • useMemo() makes sure the provider value doesn’t change unnecessarily when nothing has actually changed
<UserContext.Provider value={user}>
<ThemeContext.Provider value={theme}>
<App />
</ThemeContext.Provider>
</UserContext.Provider>

Use more than one context when the data is about different things. This way, updates stay smaller and easier to understand.

Examples:

  • AuthContext
  • ThemeContext
  • SettingsContext
<AppContext.Provider value={{ user, theme, locale, notifications, cart, permissions }}>

This creates one giant context that holds everything. Any small change can cause many components to re-render, and the code becomes harder to manage.

Pattern:

graph TD P["Provider passes state + updater"] --> C["Child reads context"] C --> U["Child calls updater"] U --> P
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 component can read the value and update it too, but the provider is still the one that actually 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:

  • Easier and cleaner usage
  • A clear error message if someone forgets to add the provider
  • One single place to change how the context works later

Each context object holds:

{
currentValue,
Provider,
Consumer
}
graph TD A["Provider updates value"] --> B["React marks subtree"] B --> C["Consumers re-render"]

Important thing to know:

graph TD A["Provider value changes"] --> B["Consumers using this context"] B --> C["They can re-render"]

Problem example:

value={{ user, theme }}

If only theme changes, components that only care about user might still re-render anyway.

This happens because React only checks if the provider value is a different object, it does not check what’s actually inside the object.

Split Context

Use smaller, separate contexts like UserContext and ThemeContext instead of putting everything in one big object.

Memoize Value

Use useMemo so the provider value object stays the same when nothing has changed.

Avoid Large Objects

Smaller, focused values mean fewer unnecessary updates for components reading them.

const value = useMemo(() => ({ user, setUser }), [user]);

A few more tips:

  • Keep the provider value as small and simple as possible
  • Only pass functions separately if you really need to
  • Splitting a context into smaller pieces is usually better than trying to memoize one huge object
  • If the data doesn’t really need to be shared, just use local state in the component instead
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 small update changes the whole object, which can cause many components to re-render.

function ThemeButton() {
const { theme, setTheme } = useContext(ThemeContext);
return (
<button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>Current: {theme}</button>
);
}

The provider owns the data. Other components just read the value and call the update function when needed. This is the cleanest way to use Context if you’re just starting out.

FeaturePropsContext
ScopeLocalShared across a group of components
ControlYou can clearly see where data comes fromLess obvious, data appears without being passed directly
ReusabilityHighMedium
DebuggingEasyA bit harder

Use props when data only needs to travel one or two levels. Use context when many components, even ones far apart, need the same value.

FeatureContextRedux
How complex it isLowHigh
PerformanceMediumHigh
DevTools supportLimitedVery good
Best forSmall to medium appsLarge apps

Context is not a complete state management tool by itself. If you need logging, time travel debugging, complex async logic, or large shared state updates, something like Redux Toolkit or Zustand might suit you 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 helpful when your updates are more complex than just simple “set this value” functions. It keeps the way your state changes predictable and organized.

function StoreProvider({ children }) {
const [state, dispatch] = useReducer(reducer, { count: 0 });
return <StoreContext.Provider value={{ state, dispatch }}>{children}</StoreContext.Provider>;
}

In real projects, this wrapper keeps the provider setup code separate from your main app, and makes the context easy to reuse anywhere.

function useStore() {
const context = useContext(StoreContext);
if (!context) {
throw new Error("useStore must be used within StoreProvider");
}
return context;
}

This is a good pattern to use in real projects because it stops things from silently breaking if someone forgets to add the provider.

  • Using context for every little piece of state
  • Passing huge context objects with too much data inside
  • Forgetting to use useMemo for the object passed to providers
  • Reading context in places that don’t actually need to use it
  • Stacking too many providers without any clear structure
  • Putting fast-changing data inside one big top-level context
  • Building contexts without custom hooks or checks for missing providers
  • Split contexts based on what they’re for: auth, theme, settings, and so on
  • Keep provider values stable using useMemo
  • Keep your update logic in one provider or reducer
  • Make a custom hook for each context
  • Use props for simple data that just goes from parent to child
graph TD APP["App"] --> AUTH["AuthProvider"] APP --> THEME["ThemeProvider"] APP --> SETTINGS["SettingsProvider"]

This kind of setup is common in real projects because each provider is responsible for just one thing.

Example layout:

function App() {
return (
<AuthProvider>
<ThemeProvider>
<SettingsProvider>
<Router />
</SettingsProvider>
</ThemeProvider>
</AuthProvider>
);
}
Context = broadcast system

When the provider’s value changes, React sends that change out to all the components that are using it.

graph LR P["Provider pushes value"] --> C["Consumers subscribe to value"]

Fiber integration:

Each fiber tracks context dependencies

React looks at the provider’s value to decide which components that depend on it actually need to re-render.

graph TD A["Provider value changes"] --> B["React compares old vs new (Object.is)"] B --> C["Marks dependent consumers"] C --> D["Schedules re-render"]

This is why creating a new object like value={{ user }} every time can cause extra, unneeded work, even if the actual data inside looks the same as before.

  • Too many updates happening.
  • Complex state logic.
  • Hard to debug issues.

If this happens, you can consider:

  • Redux
  • Zustand
  • Recoil

In short:

  • Use Context for sharing values and light coordination between components
  • Use Zustand or Redux when you need a more central place to manage app state, with better tools and more control over updates

Before you create a context, ask yourself:

  • Do more than one far-apart components need this data?
  • Does this data need to be shared with a whole group of components?
  • Does this make more sense as shared settings, rather than state inside one component?
  • Will this value change often enough to cause performance problems?

If the answer is no, just use props or local state instead.