React 39 ⚛️ Providing and Consuming Context
A context object on its own does nothing. It is a token — an identity that a provider and a consumer agree on. The provider gives the token a value, the consumer reads that value, and the connection between them is established by React walking the tree. Neither end knows where the other is. The provider does not know which components will read the value. The consumer does not know where the value came from. They both reference the same context object, and React does the rest.
Providing and consuming are the two halves of that connection. Providing means rendering the context object as a provider component with a value prop. The value can be anything: a string, a number, an object, a function, a combination of all of these. The provider sits at some level of the tree, and every descendant that reads that context receives the value. Consuming means calling useContext with the context object. The hook returns the nearest provider’s value above the calling component. If there is no provider, the hook returns the default value that was passed to createContext.
The mechanics are simple, but the implications are not. A provider’s value is not static — it can change when the provider’s state changes, and when it changes, every consumer re-renders. A consumer does not declare its dependency in its props — the dependency is implicit in the useContext call. A provider can be nested inside another provider of the same context, and the nearest one wins. These behaviors determine how context performs and how it should be structured.
This chapter covers three areas. First, why providing and consuming exist as separate operations — what the separation enables and why the provider-consumer relationship is not a parent-child relationship in the usual sense. Second, how to provide a value and how to consume it — the syntax of the provider, the useContext hook, the default-value behavior, and the nesting rules. Third, how to structure providers and consumers for correctness and performance — memoizing values, splitting contexts, placing providers as deep as necessary, and avoiding the common mistakes that turn context into a performance problem. The chapter ends with a complete example session, a quick reference, best practices, common pitfalls, real-world examples, and diagrams showing the provider-consumer relationship.
Key point: A provider supplies a value; a consumer reads the nearest provider’s value above it in the tree. The provider and consumer do not need to be parent and child — they only need to share the same context object and have the provider somewhere above the consumer. When the provider’s value changes, every consumer of that context re-renders .
Why providing and consuming exist
The decoupling problem. In a props-based world, the component that owns data and the component that uses it must be connected by an unbroken chain of props. The owner knows who its children are, the children know who their children are, and the data travels one level at a time. Context removes that requirement. The provider does not need to know which components will consume the value. The consumer does not need to know which component provided it. They only need to reference the same context object. This decoupling is what allows a deeply nested component to receive data from an ancestor it has no direct relationship with .
The nearest-provider rule. A consumer reads the value from the nearest provider above it in the tree. This means a context can have multiple providers at different levels, and each subtree can receive a different value. A ThemeContext provider at the root might supply 'light', while a nested provider inside a specific panel supplies 'dark'. Consumers inside the panel read 'dark'; consumers outside it read 'light'. The rule is positional: the closest ancestor provider wins. This enables local overrides without affecting the rest of the tree .
The default-value fallback. When a consumer reads a context and no provider exists above it, the value is the default passed to createContext. This is the only situation in which the default is used. If any provider exists — even one that supplies undefined — the default is not used. The default is not a “base value” that providers override; it is a fallback for the absence of any provider. This distinction matters because a default that resembles real data can hide a missing provider. The component renders with the default and appears to work, but it is actually running outside the intended context .
The re-render propagation. When a provider’s value changes, every component that reads that context re-renders. This is not a bug — it is how context propagates changes. The provider’s value is a prop to an internal React mechanism, and when that prop changes, the consumers that depend on it must re-render to receive the new value. The performance question is not whether consumers re-render, but how many consumers there are and how often the value changes. A context whose value changes on every provider render will re-render every consumer on every provider render. A context whose value is stable will re-render consumers only when the value actually changes .
The implicit dependency problem. A component that calls useContext depends on that context, but the dependency is not visible in the component’s props. You cannot tell by looking at the component’s signature that it requires a ThemeProvider above it. This makes the component harder to reuse outside the provider and harder to test without wrapping it. The guarded hook pattern addresses this by throwing when the provider is missing, turning the implicit dependency into an explicit runtime requirement. But the dependency remains invisible at the type level unless the hook’s return type makes it clear .
The trade-off. Context eliminates prop drilling at the cost of implicit dependencies and re-render propagation. The provider no longer knows its consumers, which is liberating but also means the provider cannot optimize for them. The consumer no longer declares its dependency in props, which is convenient but also means the dependency is hidden. The question is not whether context is better than props, but whether the specific case — the distance between owner and consumer, the number of consumers, the frequency of changes — makes context the right tool.
a. Providing a value
A provider is a component that renders the context object with a value prop. In React 19, the context object itself is renderable. In earlier versions, the provider is the .Provider property of the context object.
The provider supplies whatever value is passed to value. The value can be a primitive:
<ThemeContext value="dark">
<App />
</ThemeContext>
It can be an object:
<AuthContext value={{ user, login, logout }}>
<App />
</AuthContext>
It can be a function:
<DispatchContext value={dispatch}>
<App />
</DispatchContext>
The provider does not transform the value. It supplies it as-is. Consumers receive exactly what the provider passes to value, unless a closer provider exists above them .
A provider can be nested inside another provider of the same context. The inner provider’s value overrides the outer provider’s value for its subtree:
<ThemeContext value="light">
<Header />
<ThemeContext value="dark">
<Sidebar />
</ThemeContext>
<Footer />
</ThemeContext>
Header and Footer read 'light'. Sidebar reads 'dark'. The nesting creates a local override. This is useful for panels, modals, or any subtree that needs a different value from the rest of the application .
b. Consuming a value
A consumer is a component that calls useContext with the context object. The hook returns the value from the nearest provider above the calling component.
import { useContext } from 'react';
function ThemedButton() {
const theme = useContext(ThemeContext);
return <button className={theme}>Click</button>;
}
ThemedButton reads ThemeContext. If a provider exists above it, the hook returns that provider’s value. If no provider exists, the hook returns the default value passed to createContext .
The hook can be called multiple times with different contexts:
function Profile() {
const theme = useContext(ThemeContext);
const user = useContext(AuthContext);
return <div className={theme}>{user.name}</div>;
}
Each call is independent. The component re-renders when either context value changes. This is the same as calling useState multiple times — each call is a separate slot in React’s internal tracking, and the order of the calls must be consistent across renders .
A consumer can also be a class component’s contextType property, but this is the legacy API. Function components with useContext are the current approach. The useContext hook cannot be called conditionally, inside loops, or inside nested functions. It must be called at the top level of the component, before any early returns .
c. Structuring providers and consumers
The placement of providers and the shape of context values determine how context performs. Three practices make the difference between a context that scales and one that re-renders the tree on every change.
Memoize object values. A provider that passes an object literal creates a new object on every render. React compares the context value by identity, so a new object means the value has changed, even if the object’s contents are identical. Every consumer re-renders. useMemo fixes this by returning the same object reference until the dependencies change:
function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const value = useMemo(
() => ({ user, login, logout }),
[user]
);
return <AuthContext value={value}>{children}</AuthContext>;
}
The value changes only when user changes. Provider re-renders for other reasons do not cascade to consumers .
Split contexts by concern. A single context that carries unrelated values re-renders every consumer when any value changes. If AppContext carries user and theme, changing theme re-renders every component that reads AppContext, even those that only use user. Splitting into UserContext and ThemeContext means theme changes re-render only theme consumers. The cost is more providers, but the benefit is a smaller re-render blast radius .
<UserContext value={userValue}>
<ThemeContext value={themeValue}>
<App />
</ThemeContext>
</UserContext>
Place providers as deep as necessary. A provider does not need to wrap the entire application. If only one subtree needs the context, the provider can be placed at the root of that subtree. This limits the number of consumers and keeps the context’s scope narrow. The deeper the provider, the fewer components are affected by its value changes. Providers that wrap the entire app should be reserved for values that genuinely need to be globally accessible .
function SettingsPanel() {
return (
<ThemeContext value="dark">
<SettingsForm />
</ThemeContext>
);
}
SettingsForm and its descendants read 'dark'. The rest of the application is unaffected. When SettingsPanel unmounts, the provider and its value are removed, and consumers outside it are never touched.
Complete Example Session
// ============================================
// PART 1: THE CONTEXT DEFINITION
// ============================================
import { createContext, useContext, useMemo, useState } from 'react';
interface AuthContextValue {
user: { name: string; email: string } | null;
login: (name: string) => void;
logout: () => void;
}
const AuthContext = createContext<AuthContextValue | null>(null);
// ============================================
// PART 2: THE PROVIDER COMPONENT
// ============================================
// The provider owns the auth state and supplies
// a memoized value to every descendant.
function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<{ name: string; email: string } | null>(null);
const login = (name: string) => {
setUser({ name, email: `${name.toLowerCase()}@example.com` });
};
const logout = () => {
setUser(null);
};
const value = useMemo(
() => ({ user, login, logout }),
[user]
);
return <AuthContext value={value}>{children}</AuthContext>;
}
// ============================================
// PART 3: THE GUARDED CONSUMER HOOK
// ============================================
// The hook throws if the provider is missing.
// Consumers never see null.
function useAuth(): AuthContextValue {
const context = useContext(AuthContext);
if (context === null) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}
// ============================================
// PART 4: A CONSUMER COMPONENT
// ============================================
// The component reads the context and renders
// based on the user's authentication state.
function UserBadge() {
const { user, login, logout } = useAuth();
if (!user) {
return <button onClick={() => login('Guest')}>Log in</button>;
}
return (
<div>
<span>{user.name}</span>
<button onClick={logout}>Log out</button>
</div>
);
}
// ============================================
// PART 5: WIRING THE PROVIDER INTO THE TREE
// ============================================
// The provider wraps the components that need auth.
// UserBadge is a descendant and can read the context.
function App() {
return (
<AuthProvider>
<Header>
<UserBadge />
</Header>
<Main />
</AuthProvider>
);
}
// ============================================
// PART 6: NESTED PROVIDERS — LOCAL OVERRIDE
// ============================================
// A nested provider overrides the value for its
// subtree. Consumers inside the nested provider
// read the inner value; consumers outside read
// the outer value.
function ThemeShowcase() {
return (
<ThemeContext value="light">
<Header /> {/* reads 'light' */}
<ThemeContext value="dark">
<Sidebar /> {/* reads 'dark' */}
<Content /> {/* reads 'dark' */}
</ThemeContext>
<Footer /> {/* reads 'light' */}
</ThemeContext>
);
}
// ============================================
// PART 7: MEMOIZATION PREVENTS CASCADING RE-RENDERS
// ============================================
// Without useMemo, the provider creates a new
// object on every render, and every consumer
// re-renders even when user has not changed.
// WITH useMemo:
// - value identity is stable until user changes
// - consumers re-render only when user changes
// - provider re-renders for other reasons do
// not cascade to consumers
const value = useMemo(
() => ({ user, login, logout }),
[user]
);
// ============================================
// PART 8: SPLITTING CONTEXTS BY CONCERN
// ============================================
// A single context carrying user and theme would
// re-render every consumer when either changes.
// Splitting means theme changes re-render only
// theme consumers, and user changes re-render
// only user consumers.
const UserContext = createContext<UserValue | null>(null);
const ThemeContext = createContext<ThemeValue | null>(null);
function Root() {
return (
<UserContext value={userValue}>
<ThemeContext value={themeValue}>
<App />
</ThemeContext>
</UserContext>
);
}
// ============================================
// PART 9: PLACING PROVIDERS DEEP
// ============================================
// A provider does not need to wrap the whole app.
// If only a subtree needs the context, place the
// provider at the root of that subtree.
function SettingsPanel() {
return (
<ThemeContext value="dark">
<SettingsForm />
<SettingsPreview />
</ThemeContext>
);
}
// SettingsForm and SettingsPreview read 'dark'.
// The rest of the app is unaffected. When the
// panel unmounts, the provider and its value
// are removed from the tree.
// ============================================
// PART 10: THE CONSUMER'S PERSPECTIVE
// ============================================
// The consumer does not know where the provider
// is. It only knows that the value is available.
function SettingsForm() {
const theme = useContext(ThemeContext);
// theme is 'dark' because the nearest provider
// above SettingsForm supplies 'dark'.
return <form className={theme}>...</form>;
}
// The consumer's dependency is implicit. The
// guarded hook makes the requirement explicit:
// if the provider is missing, useTheme throws.
The ten parts show the provider-consumer relationship from both ends: the provider owns state and supplies a memoized value; the consumer reads the nearest provider’s value; nesting creates local overrides; memoization prevents cascading re-renders; splitting contexts limits the blast radius; and placing providers deep keeps scope narrow.
Quick Reference
Providing a Value
| Element | Meaning |
|---|---|
<Context value={v}> | React 19 syntax; context object as provider |
<Context.Provider value={v}> | Legacy syntax; still supported |
value prop | The value consumers receive |
| Nesting | Inner provider overrides outer for its subtree |

Consuming a Value
| Element | Meaning |
|---|---|
useContext(Context) | Returns nearest provider’s value |
| No provider | Returns createContext default |
| Multiple calls | Each call is independent; component re-renders on any change |
| Call location | Top level of component; never conditional |
The Nearest-Provider Rule
| Situation | Value received |
|---|---|
| One provider above | That provider’s value |
| Multiple providers above | Nearest ancestor’s value |
| No provider above | Default from createContext |
Provider with undefined | undefined — default is not used |
Re-render Behavior
| Change | Consumers affected |
|---|---|
| Provider value changes | All consumers of that context |
| Provider re-renders, value unchanged | No consumers (if value is memoized) |
| Provider re-renders, value is new object | All consumers |
| Unrelated state changes | No consumers |
Structuring Providers
| Practice | Benefit |
|---|---|
| Memoize object values | Prevents cascading re-renders |
| Split contexts by concern | Limits re-render blast radius |
| Place providers deep | Narrows scope, fewer consumers |
| Guard the consuming hook | Makes missing provider explicit |
Best Practices
✅ Do This:
// Memoize object values
const value = useMemo(() => ({ user, login }), [user]); // ✅
// Split contexts by concern
const UserContext = createContext(null); // ✅
const ThemeContext = createContext(null); // ✅
// Place providers deep
<ThemeContext value="dark"><SettingsPanel /></ThemeContext> // ✅
// Guard the consuming hook
if (context === null) throw new Error('useAuth requires AuthProvider'); // ✅
// Use the nearest-provider rule for local overrides
<ThemeContext value="light"><ThemeContext value="dark">...</> // ✅
// Keep the context object private
export function useAuth() { ... } // ✅
❌ Don’t Do This:
// Don't create a new object on every render
<AuthContext value={{ user, login, logout }}> // ❌
// Don't put unrelated values in one context
const AppContext = createContext({ user, theme, cart }); // ❌
// Don't wrap the entire app when only a subtree needs it
<ThemeContext value="dark"><App /></ThemeContext> // ❌
// Don't skip the null guard
const { user } = useContext(AuthContext); // ❌
// Don't call useContext conditionally
if (isLoggedIn) { const user = useContext(AuthContext); } // ❌
// Don't export the raw context object
export const AuthContext = createContext(null); // ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| All consumers re-render on provider render | Object value not memoized | Wrap value in useMemo |
| Consumer reads default instead of provider value | Provider is not above consumer in tree | Move provider higher or check nesting |
| Nested provider does not override | Inner provider is not a descendant of consumer | Verify tree structure |
| Theme change re-renders user consumers | Single context carries both values | Split contexts by concern |
| Provider wraps entire app unnecessarily | Convenience | Place provider at subtree root |
| Missing provider goes unnoticed | Default value looks like real data | Use null default and guarded hook |
useContext called conditionally | Confusion about Hook rules | Move call to top level |
| Consumer re-renders on every provider render | Provider re-renders frequently | Memoize value and stabilize functions |
Real-World Examples
1. Provider at Root
<AuthProvider><App /></AuthProvider>
2. Provider in a Subtree
<ThemeContext value="dark"><SettingsPanel /></ThemeContext>
3. Consumer with Hook
const { user } = useAuth();
4. Nested Provider Override
<ThemeContext value="light">
<ThemeContext value="dark"><Sidebar /></ThemeContext>
</ThemeContext>
5. Memoized Value
const value = useMemo(() => ({ user, login }), [user]);
6. Split Contexts
<UserContext value={userValue}>
<ThemeContext value={themeValue}><App /></ThemeContext>
</UserContext>
7. Guarded Hook
if (context === null) throw new Error('useAuth requires AuthProvider');
8. Multiple useContext Calls
const theme = useContext(ThemeContext);
const user = useContext(AuthContext);
9. Default Value Fallback
const ThemeContext = createContext('light');
// Consumer outside any provider reads 'light'
10. Provider with Function Value
<DispatchContext value={dispatch}><App /></DispatchContext>
Visual
The Provider-Consumer Relationship
┌──────────────────────────────────────────────────────────────┐
│ PROVIDER AND CONSUMER │
│ │
│ <AuthContext value={value}> ← provider │
│ │ │
│ ├── <Header /> ← does not consume │
│ │ │
│ ├── <Main> │
│ │ │ │
│ │ └── <UserBadge /> ← consumer │
│ │ useContext(AuthContext) → value │
│ │ │
│ └── <Footer /> ← does not consume │
│ │
│ The provider and consumer are not parent and child. │
│ The provider is an ancestor. React walks the tree to │
│ find the nearest provider above the consumer. │
│ │
└──────────────────────────────────────────────────────────────┘
The Nearest-Provider Rule
┌──────────────────────────────────────────────────────────────┐
│ NESTED PROVIDERS │
│ │
│ <ThemeContext value="light"> │
│ │ │
│ ├── <Header /> → reads 'light' │
│ │ │
│ ├── <ThemeContext value="dark"> │
│ │ │ │
│ │ ├── <Sidebar /> → reads 'dark' │
│ │ └── <Content /> → reads 'dark' │
│ │ │
│ └── <Footer /> → reads 'light' │
│ │
│ The nearest provider above each consumer wins. │
│ Sidebar and Content are inside the dark provider, │
│ so they read 'dark'. Header and Footer are outside it, │
│ so they read 'light'. │
│ │
└──────────────────────────────────────────────────────────────┘
Memoization Prevents Cascading Re-renders
┌──────────────────────────────────────────────────────────────┐
│ WITHOUT MEMOIZATION │
│ │
│ Provider renders │
│ → new object { user, login, logout } │
│ → context value identity changes │
│ → ALL consumers re-render │
│ → even if user did not change │
│ │
│ WITH MEMOIZATION │
│ │
│ Provider renders │
│ → useMemo returns cached object (user unchanged) │
│ → context value identity is stable │
│ → consumers DO NOT re-render │
│ │
│ const value = useMemo( │
│ () => ({ user, login, logout }), │
│ [user] │
│ ); │
│ │
│ The dependency array [user] means the value changes │
│ only when user changes. Other provider re-renders do │
│ not cascade to consumers. │
│ │
└──────────────────────────────────────────────────────────────┘
Splitting Contexts Limits the Blast Radius
┌──────────────────────────────────────────────────────────────┐
│ ONE CONTEXT vs SPLIT CONTEXTS │
│ │
│ ONE CONTEXT: │
│ <AppContext value={{ user, theme }}> │
│ Theme change → every consumer re-renders │
│ (even components that only use user) │
│ │
│ SPLIT CONTEXTS: │
│ <UserContext value={userValue}> │
│ <ThemeContext value={themeValue}> │
│ Theme change → only theme consumers re-render │
│ User change → only user consumers re-render │
│ </ThemeContext> │
│ </UserContext> │
│ │
│ The cost is more providers. The benefit is a smaller │
│ re-render blast radius. Each context changes only when │
│ its own slice changes. │
│ │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Provider | Renders context object with a value prop |
| Consumer | Calls useContext with the context object |
| Value received | Nearest provider’s value above the consumer |
| No provider | Default value from createContext |
| Nesting | Inner provider overrides outer for its subtree |
| Re-render trigger | Provider value identity changes |
| Memoization | useMemo required for object values |
| Splitting | Separate contexts by concern to limit re-renders |
| Provider placement | As deep as necessary to narrow scope |
| Guarded hook | Throws when provider is missing |
Key takeaways:
- Providing and consuming are decoupled. The provider does not know which components consume the value. The consumer does not know which component provided it. They only share the context object.
- The nearest provider wins. A context can have multiple providers at different levels. Each consumer reads the value from the closest provider above it. This enables local overrides without affecting the rest of the tree.
- The default is only for the absence of any provider. If a provider exists — even one supplying
undefined— the default is not used. A default that resembles real data can hide a missing provider. - Memoize object values. A new object on every provider render means a new context value on every render, and every consumer re-renders.
useMemokeeps the identity stable until the contents change. - Split contexts by concern. A context that carries unrelated values re-renders every consumer when any value changes. Separate contexts limit the re-render blast radius.
- Place providers as deep as necessary. A provider does not need to wrap the entire application. The deeper the provider, the fewer consumers it affects and the narrower its scope.
- The consumer’s dependency is implicit. A component that calls
useContextdepends on that context, but the dependency is not visible in its props. The guarded hook makes the requirement explicit at runtime. - Re-renders propagate to all consumers. When a provider’s value changes, every consumer of that context re-renders. The performance question is how many consumers there are and how often the value changes.
Remember: Providing and consuming are the two operations that make context work. A provider supplies a value; a consumer reads the nearest provider’s value above it. The provider and consumer do not need to be parent and child — they only need to share the same context object and have the provider somewhere above the consumer. The value changes propagate to every consumer, so the performance of context depends on how the value is memoized, how the contexts are split, and where the providers are placed. A well-structured context has a stable value, a narrow scope, and a guarded hook that makes the provider requirement explicit. The provider stays unaware of its consumers, the consumer stays unaware of its provider, and the context object connects them without either end needing to know the other.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!