Split Context
Use smaller, separate contexts like UserContext and ThemeContext instead of putting everything in one big object.
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.
Context gives you a provider and consumer setup.
createContext()useContext(): the modern way to read context inside function componentsUse context for data that:
Good examples:
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.
useContext()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().
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 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 valuesetTheme is the function used to change ituseMemo() 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:
AuthContextThemeContextSettingsContext<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:
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:
Each context object holds:
{ currentValue, Provider, Consumer}Important thing to know:
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:
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.
| Feature | Props | Context |
|---|---|---|
| Scope | Local | Shared across a group of components |
| Control | You can clearly see where data comes from | Less obvious, data appears without being passed directly |
| Reusability | High | Medium |
| Debugging | Easy | A 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.
| Feature | Context | Redux |
|---|---|---|
| How complex it is | Low | High |
| Performance | Medium | High |
| DevTools support | Limited | Very good |
| Best for | Small to medium apps | Large 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.
useMemo for the object passed to providersuseMemoThis 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 systemWhen the provider’s value changes, React sends that change out to all the components that are using it.
Fiber integration:
Each fiber tracks context dependenciesReact looks at the provider’s value to decide which components that depend on it actually need to 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.
If this happens, you can consider:
In short:
Before you create a context, ask yourself:
If the answer is no, just use props or local state instead.