React 38 ⚛️ Creating Context with createContext
Context begins with a single function call. createContext takes a default value and returns an object that components can provide and read. That object does not hold state, does not subscribe to anything, and does not re-render anything on its own. It is a token — a reference that connects a provider somewhere up the tree to a consumer somewhere down the tree. The state lives in the provider component, the reading happens in the consumer component, and the context object is the channel between them .
The default value passed to createContext is not the value most consumers will see. It is a fallback. It applies only when no matching provider exists above the consumer in the tree. If a provider is present, its value prop overrides the default entirely. This distinction matters because a default value that looks like real data can hide a missing provider. A ThemeContext created with 'light' as the default will silently render light-themed components even if the developer forgot to wrap the tree in a ThemeContext.Provider. The component works, but it works with the wrong value .
The function is called outside any component. It is not a Hook, it does not run during render, and it does not depend on the component tree. You call it once at module scope, export the returned object, and import it wherever a provider or consumer needs it . This separation — context definition outside React, context value inside React — is what makes context portable across files and reusable across component subtrees.
This chapter covers three areas. First, why createContext exists — what problem it solves that composition alone cannot, and why a context object is necessary as an intermediary. Second, how to create and type a context — the mechanics of calling createContext, choosing a default value, and giving the context a TypeScript type that enforces correct usage. Third, how to wrap context creation in a custom hook and provider — the pattern that makes context safe to consume and easy to refactor. The chapter ends with a complete example session, a quick reference, best practices, common pitfalls, real-world examples, and diagrams showing the relationship between the context object, its provider, and its consumers.
Key point: createContext returns a context object that represents a channel between a provider and its consumers. The default value is a fallback for when no provider exists, not the value consumers normally receive. The context object holds no state itself — the state lives in the provider component that supplies the value prop .
Why createContext exists
The composition ceiling. Component composition solves prop drilling when the source component can render the consumer directly. But composition has a structural requirement: the component that owns the data must be able to create the element that needs the data. When the consumer must be rendered deep inside a component that controls its own children — a layout that interleaves multiple sections, a form that places fields in specific slots, a library component that expects to control its internal structure — the source cannot reach in and place the consumer where it belongs. The data still needs a channel that crosses component boundaries without restructuring the tree. createContext provides that channel .
The token problem. A channel needs two ends: something that provides a value and something that reads it. If the provider and consumer are in different files, they need a shared reference to the same channel. That reference is the context object. It is not the value, not the state, not the provider — it is the identity of the channel. createContext creates that identity. Without it, there would be no way for a consumer in one file to know which provider in another file it should read from. The context object is the token that both ends agree on .
The default value problem. Every channel needs a behavior when no provider is connected. A consumer that reads a context with no provider above it needs to receive something. The default value is that something. It is static, it never changes, and it is explicitly documented as a last resort. The alternative — making the consumer crash when no provider exists — would be less forgiving for cases where a default is meaningful. A ThemeContext with a 'light' default allows components to render outside a provider with a sensible fallback. A AuthContext with null as the default forces the consumer to handle the unauthenticated case explicitly. The choice of default value is a design decision that signals what the context means when it is absent .
The typing problem. In JavaScript, a context can carry any value. In TypeScript, the context object needs a type that describes what consumers will receive. Without explicit typing, useContext returns the type of the default value, which may be too narrow. If the default is null but the provider supplies an object, the consumer’s type says null and every property access fails type-checking. createContext accepts a type parameter that declares the context’s shape independently of its default. This lets a context be created with null as the default while consumers receive the full type through the provider, with a runtime guard ensuring the null case never reaches them .
The module scope problem. createContext must be called outside any component. If it were called inside a component, every render would create a new context object, and the identity that connects providers to consumers would change on every render. The provider and consumer would no longer agree on which channel they are using. By calling createContext at module scope, the context object is created once, its identity is stable, and every provider and consumer in the module tree references the same token .
The trade-off. Context introduces an implicit dependency. A component that calls useContext(ThemeContext) no longer shows theme in its props. The dependency is real, but it is not visible in the component’s signature. This makes the component harder to reuse outside the provider and harder to test without wrapping it. The default value can mask the missing provider, turning a structural error into a silent fallback. The context object solves the channel problem, but it creates a new class of problem: dependencies that are not declared where they are used.
a. Creating and typing a context
The call to createContext takes one argument: the default value. This value determines the type of the context if no type parameter is supplied, and it determines what consumers receive when no provider is present.
A context for a simple string theme can be created with a string default:
import { createContext } from 'react';
const ThemeContext = createContext('light');
The inferred type of ThemeContext is Context<string>. A consumer calling useContext(ThemeContext) receives a string. The default value 'light' is returned when no provider exists above the consumer .
For contexts that carry objects — especially objects with functions — the default value should be chosen carefully. A default object that contains placeholder functions can hide bugs: the functions exist, they do nothing, and the component renders as if the provider were working. A safer approach is to use null as the default and let TypeScript force the consumer to handle the null case, or to use a custom hook that throws when the context is null .
interface AuthContextValue {
user: User | null;
isAuthenticated: boolean;
login: (token: string) => Promise<void>;
logout: () => void;
}
const AuthContext = createContext<AuthContextValue | null>(null);
The type parameter AuthContextValue | null declares that the context value is either the full auth object or null. The default is null. When a provider supplies an AuthContextValue, consumers receive that value. When no provider exists, consumers receive null, and TypeScript forces them to check for it before accessing properties .
This pattern — null default with a union type — is the recommended approach for contexts that carry critical data. The alternative — a fabricated default object with no-op functions — makes the component appear to work when it is actually running without a provider. The bug is silent because the fake functions exist and the fake data is present. The null default makes the missing provider an explicit error that TypeScript catches at compile time and a runtime guard catches at render time .
b. The provider component
The context object does not provide a value. It only identifies the channel. To provide a value, a component must render the context object as a provider. In React 19, the context object itself is renderable as a provider. In earlier versions, the provider is accessed through the .Provider property .
function App() {
const [theme, setTheme] = useState('dark');
return (
<ThemeContext value={theme}>
<Layout />
</ThemeContext>
);
}
The value prop is what consumers receive. It can be any type. When the value changes, every component that reads the context re-renders. This is the mechanism that propagates context updates through the tree .
For contexts that carry objects, the value prop must be stable across renders when the object’s contents have not changed. If the provider creates a new object on every render — value={{ theme, setTheme }} — every consumer re-renders on every provider render, even when theme and setTheme are unchanged. The object identity is new, so React considers the context value changed .
The fix is useMemo:
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const value = useMemo(
() => ({ theme, setTheme }),
[theme]
);
return (
<ThemeContext value={value}>
{children}
</ThemeContext>
);
}
useMemo caches the object and returns the same reference until theme changes. Consumers re-render only when theme actually changes, not on every provider render. If the value contains functions, useCallback or useMemo around the functions is also required to keep their references stable .
c. The guarded custom hook
The third piece of the pattern is a custom hook that wraps useContext and throws when the context is missing. This converts the null default from a silent fallback into an explicit error.
function useAuth(): AuthContextValue {
const context = useContext(AuthContext);
if (context === null) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}
The hook’s return type is AuthContextValue, not AuthContextValue | null. TypeScript no longer requires consumers to check for null. The runtime check throws if the provider is missing, so the null case cannot reach the consumer. The error message names the hook and the missing provider, making the structural mistake visible in the console .
This pattern — context with null default, provider component, guarded custom hook — is the canonical way to create a context that is safe to consume. The context definition, the provider, and the hook are often placed in the same file and exported together. Consumers import the hook, not the context object. The context object remains private to the module, preventing consumers from bypassing the guard by calling useContext(AuthContext) directly .
Complete Example Session
// ============================================
// PART 1: THE CONTEXT INTERFACE
// ============================================
// The context carries both state and actions.
// The state is the current theme; the actions
// are the functions that change it.
import { createContext, useContext, useMemo, useState } from 'react';
interface ThemeContextValue {
theme: string;
setTheme: (next: string) => void;
}
// ============================================
// PART 2: CREATING THE CONTEXT WITH NULL DEFAULT
// ============================================
// The default is null. This forces consumers to
// handle the missing-provider case, either by
// checking for null or by using a guarded hook.
const ThemeContext = createContext<ThemeContextValue | null>(null);
// ============================================
// PART 3: THE PROVIDER COMPONENT
// ============================================
// The provider owns the state and supplies the
// value. The value is memoized so consumers do
// not re-render when the provider re-renders for
// unrelated reasons.
function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState('light');
const value = useMemo(
() => ({ theme, setTheme }),
[theme]
);
return (
<ThemeContext value={value}>
{children}
</ThemeContext>
);
}
// ============================================
// PART 4: THE GUARDED CUSTOM HOOK
// ============================================
// The hook throws if the context is null. Consumers
// never see null. TypeScript knows the return type
// is ThemeContextValue, not ThemeContextValue | null.
function useTheme(): ThemeContextValue {
const context = useContext(ThemeContext);
if (context === null) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
}
// ============================================
// PART 5: A CONSUMER COMPONENT
// ============================================
// The consumer calls useTheme and destructures the
// value and the setter. No null check is needed.
function ThemeToggle() {
const { theme, setTheme } = useTheme();
return (
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Current: {theme}
</button>
);
}
// ============================================
// PART 6: WIRING THE PROVIDER INTO THE TREE
// ============================================
// The provider sits above every component that needs
// the theme. The consumer is rendered as a descendant.
function App() {
return (
<ThemeProvider>
<Header />
<Main>
<ThemeToggle />
</Main>
</ThemeProvider>
);
}
// ============================================
// PART 7: WHAT HAPPENS WHEN THE PROVIDER IS MISSING
// ============================================
// If ThemeToggle is rendered outside ThemeProvider,
// useTheme calls useContext, receives null, and
// throws the error. The component does not render
// with a fallback theme. The mistake is visible.
// <ThemeToggle /> // ❌ throws: "useTheme must be used within a ThemeProvider"
// ============================================
// PART 8: WHY THE VALUE IS MEMOIZED
// ============================================
// Without useMemo, this line creates a new object
// on every ThemeProvider render:
//
// <ThemeContext value={{ theme, setTheme }}>
//
// The object identity changes, React considers the
// context value changed, and every consumer re-renders.
//
// With useMemo, the object identity is stable until
// theme changes. Consumers re-render only when the
// theme value actually changes.
// ============================================
// PART 9: THE SEPARATE CONTEXT FILE PATTERN
// ============================================
// In larger apps, the context, provider, and hook
// are placed in their own file and exported together.
// theme-context.tsx
// export function ThemeProvider({ children }) { ... }
// export function useTheme() { ... }
//
// Consumers import { useTheme } from './theme-context'.
// They do not import the context object itself.
// ============================================
// PART 10: THE CONTEXT OBJECT AS A PRIVATE TOKEN
// ============================================
// ThemeContext is not exported. It is an implementation
// detail of the module. Consumers cannot bypass the
// guard by calling useContext(ThemeContext) directly.
// The only way to read the context is through useTheme,
// which enforces the provider requirement.
The ten parts show the full pattern: defining the context type, creating the context with a null default, supplying a memoized value through a provider, guarding consumption with a custom hook, and keeping the context object private to the module.
Quick Reference
createContext Signature
| Parameter | Meaning |
|---|---|
defaultValue | Fallback when no provider exists; static, never changes |
| Returns | Context object with .Provider (legacy) and .Consumer (legacy) |
| Type parameter | Declares the shape consumers receive |
| Call location | Outside any component, usually module scope |
Default Value Choices
| Default | When to use | Consequence |
|---|---|---|
'light' | Meaningful fallback exists | Silent fallback if provider missing |
null | No meaningful default | TypeScript forces null check |
{} as Type | Avoid if possible | Hides missing provider bugs |
null! | Avoid if possible | Type assertion defeats safety |
The Three-Part Pattern
| Part | Purpose |
|---|---|
createContext<Type | null>(null) | Defines the context with a safe default |
| Provider component | Owns state, supplies memoized value |
| Guarded custom hook | Reads context, throws if null |
Provider Value Stability
| Value type | Memoization |
|---|---|
| Primitive string/number | No memo needed |
| Object literal | useMemo required |
| Function | useCallback or useMemo |
| Object with functions | useMemo around the object |
Best Practices
✅ Do This:
// Use null default for critical contexts
const AuthContext = createContext<AuthContextValue | null>(null); // ✅
// Memoize object values
const value = useMemo(() => ({ theme, setTheme }), [theme]); // ✅
// Guard consumption with a custom hook
if (context === null) throw new Error('useAuth must be used within AuthProvider'); // ✅
// Keep the context object private to the module
export function useAuth() { ... } // ✅
// Name the context after what it provides
const ThemeContext = createContext<ThemeContextValue | null>(null); // ✅
// Split contexts by concern
const UserContext = createContext(null); // ✅
const ThemeContext = createContext(null); // ✅
❌ Don’t Do This:
// Don't use a fabricated object default
const AuthContext = createContext<AuthContextValue>({ user: null } as any); // ❌
// Don't create a new object on every render
<ThemeContext value={{ theme, setTheme }}> // ❌
// Don't export the raw context object
export const ThemeContext = createContext(null); // ❌
// Don't skip the null check
const { user } = useContext(AuthContext); // ❌
// Don't put unrelated values in one context
const AppContext = createContext({ user, theme, cart }); // ❌
// Don't call createContext inside a component
function App() { const Ctx = createContext(null); } // ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Missing provider goes unnoticed | Default value looks like real data | Use null default and guarded hook |
| All consumers re-render on every provider render | Object value not memoized | Wrap value in useMemo |
| Consumer crashes with null error | Provider exists but value is null | Check provider logic or default |
| Context object exported raw | Convenience | Export only the hook and provider |
| Type mismatch between default and provided value | Type inferred from default | Use explicit type parameter |
createContext called in component | Confusion about where it runs | Move to module scope |
| Context value changes on every render | Function references not stable | useCallback or useMemo for functions |
| Provider wraps too much of the tree | Convenience | Render providers as deep as needed |
Real-World Examples
1. Theme Context
const ThemeContext = createContext<ThemeContextValue | null>(null);
2. Auth Context
const AuthContext = createContext<AuthContextValue | null>(null);
3. Memoized Provider Value
const value = useMemo(() => ({ theme, setTheme }), [theme]);
4. Guarded Hook
if (context === null) throw new Error('useTheme must be used within ThemeProvider');
5. Separate Context File
// theme-context.tsx
export function ThemeProvider({ children }) { ... }
export function useTheme() { ... }
6. Provider at Root
<ThemeProvider><App /></ThemeProvider>
7. Consumer Without Null Check
const { theme, setTheme } = useTheme();
8. Type Parameter on createContext
createContext<AuthContextValue | null>(null);
9. Memoized Functions in Context
const login = useCallback(async (token) => { ... }, []);
10. Split Contexts
const UserContext = createContext<UserValue | null>(null);
const ThemeContext = createContext<ThemeValue | null>(null);
Visual
The Context Object as a Channel
┌──────────────────────────────────────────────────────────────┐
│ THE CONTEXT OBJECT │
│ │
│ createContext('light') │
│ │ │
│ ▼ │
│ ┌─────────────────────────┐ │
│ │ ThemeContext │ ← token, not value │
│ │ (context object) │ │
│ └─────────────────────────┘ │
│ │ │
│ │ ┌─────────────────────────────────────────┐ │
│ │ │ PROVIDER │ │
│ └─►│ <ThemeContext value="dark"> │ │
│ │ supplies the value │ │
│ └─────────────────────────────────────────┘ │
│ │ │
│ │ value travels down │
│ ▼ │
│ ┌─────────────────────────────────────────┐ │
│ │ CONSUMER │ │
│ │ useContext(ThemeContext) → "dark" │ │
│ └─────────────────────────────────────────┘ │
│ │
│ The context object is the identity of the channel. │
│ The provider supplies the value. The consumer reads it. │
│ │
└──────────────────────────────────────────────────────────────┘
Default Value vs Provider Value
┌──────────────────────────────────────────────────────────────┐
│ WHEN THE DEFAULT IS USED │
│ │
│ Case 1: No provider exists │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ <ThemeContext value="dark"> ← NO PROVIDER │ │
│ │ (nothing) │ │
│ │ </ThemeContext> │ │
│ │ │ │
│ │ Consumer reads → DEFAULT ('light') │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ Case 2: Provider exists │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ <ThemeContext value="dark"> ← PROVIDER │ │
│ │ <Consumer /> │ │
│ │ </ThemeContext> │ │
│ │ │ │
│ │ Consumer reads → PROVIDER VALUE ('dark') │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ The default is a fallback, not the normal path. │
│ A missing provider is a structural error, not a feature. │
│ │
└──────────────────────────────────────────────────────────────┘
The Three-Part Pattern
┌──────────────────────────────────────────────────────────────┐
│ SAFE CONTEXT PATTERN │
│ │
│ 1. CREATE │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ const ThemeContext = │ │
│ │ createContext<ThemeContextValue | null>(null); │ │
│ │ │ │
│ │ null default forces explicit handling │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ 2. PROVIDE │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ function ThemeProvider({ children }) { │ │
│ │ const [theme, setTheme] = useState('light'); │ │
│ │ const value = useMemo(() => ({ theme, setTheme }), │ │
│ │ [theme]); │ │
│ │ return <ThemeContext value={value}>{children}</>; │ │
│ │ } │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ 3. GUARD │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ function useTheme() { │ │
│ │ const ctx = useContext(ThemeContext); │ │
│ │ if (ctx === null) │ │
│ │ throw new Error('useTheme requires Provider'); │ │
│ │ return ctx; │ │
│ │ } │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ Create with null. Provide a memoized value. Guard the hook. │
│ The context object stays private. Consumers use the hook. │
│ │
└──────────────────────────────────────────────────────────────┘
Memoization Prevents Cascading Re-renders
┌──────────────────────────────────────────────────────────────┐
│ WITHOUT useMemo │
│ │
│ Provider renders │
│ → creates new object { theme, setTheme } │
│ → context value identity changes │
│ → ALL consumers re-render │
│ → even if theme did not change │
│ │
│ WITH useMemo │
│ │
│ Provider renders │
│ → useMemo returns cached object (theme unchanged) │
│ → context value identity is stable │
│ → consumers DO NOT re-render │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ const value = useMemo( │ │
│ │ () => ({ theme, setTheme }), │ │
│ │ [theme] │ │
│ │ ); │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ The dependency array [theme] means the value only changes │
│ when theme changes. Provider re-renders for other reasons │
│ do not cascade to consumers. │
│ │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
createContext | Function that creates a context object from a default value |
| Context object | Token that identifies the channel; holds no state itself |
| Default value | Fallback when no provider exists; static, never changes |
| Provider | Component that supplies the value to descendants |
| Consumer | Component that reads the value with useContext |
| Type parameter | Declares the shape consumers receive |
| Null default | Safe choice for critical contexts; forces explicit handling |
| Guarded hook | Custom hook that throws when context is null |
| Memoization | useMemo required for object values to prevent cascading re-renders |
| Module scope | createContext must be called outside components |
Key takeaways:
createContextcreates a token, not a value. The returned object identifies a channel. The value comes from a provider component that renders that token as a provider.- The default value is a fallback, not the normal path. It applies only when no provider exists above the consumer. A default that looks like real data can hide a missing provider.
- Null defaults plus guarded hooks make missing providers explicit. The
nulldefault forces TypeScript to require a null check. The guarded hook throws at runtime if the check fails, converting a silent fallback into a visible error. - Object values must be memoized. A provider that creates a new object on every render forces every consumer to re-render, even when the object’s contents are unchanged.
useMemokeeps the identity stable until the contents change. - The context object stays private to the module. Export the provider and the guarded hook, not the context object. Consumers should not be able to bypass the guard by calling
useContextdirectly. - Memoize functions as well as objects. A function defined inside the provider has a new identity on every render.
useCallbackoruseMemoaround the function keeps its reference stable. - Split contexts by concern. A single context that carries unrelated values re-renders every consumer when any value changes. Separate contexts let consumers re-render only when their specific slice changes.
- The three-part pattern is canonical. Create with a null default, provide a memoized value, guard the hook. This pattern makes context safe to consume and easy to refactor.
Remember: createContext is the starting point for React’s context mechanism, but it is only the first of three parts. The context object it returns is a token — an identity that connects providers to consumers. The provider supplies the value, and the guarded hook ensures that value is present before it is used. The default value passed to createContext is a fallback for the rare case where no provider exists, not the value that most consumers will see. A context created with a null default and consumed through a guarded hook makes the provider requirement explicit: if the provider is missing, the error is loud, immediate, and traceable. The context object itself stays private, the provider stays memoized, and the consumer stays unaware of the machinery. That separation — token, provider, guard — is what makes context a safe and predictable tool for sharing state across the component tree.
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!