Provider Pattern in React
Master the React Provider pattern with Context — avoid prop drilling, implement Theme and Brand providers, nest multiple contexts, React 19 use() API, auth/i18n/cart use cases, and re-render pitfalls.
Introduction
The Provider pattern publishes shared data and behavior to a subtree through React Context. A Provider component wraps part of your tree; any descendant consumes the value via useContext (or React 19's use API) without passing props through every intermediate layer.
This guide covers:
- Definition & purpose — why providers exist and what problems they solve
- Core concepts — Context, boundaries, providers, consumers
- Implementation steps — from
createContextto custom hooks - React 19 modernizations — Context as Provider, the
useAPI - Practical examples — Theme, Brand (with fetch), nested providers
- Use cases — auth, i18n, cart, feature flags, A/B testing
- Best practices & pitfalls — scope, segregation, re-renders
Quick index
1. Definition & purpose
A Provider wraps a component hierarchy and injects a value into React's context system. Descendants read that value without the parent chain manually forwarding props.
Before — prop drilling
function App() {
const [theme, setTheme] = useState<'light' | 'dark'>('light')
return <Layout theme={theme} onToggleTheme={() => setTheme((t) => (t === 'light' ? 'dark' : 'light'))} />
}
function Layout({ theme, onToggleTheme }: ThemeProps) {
return (
<main>
<Sidebar theme={theme} onToggleTheme={onToggleTheme} />
<Content theme={theme} />
</main>
)
}
// … five more layers before ThemeToggle receives theme + onToggleThemeAfter — Provider pattern
function App() {
return (
<ThemeProvider>
<Layout />
</ThemeProvider>
)
}
function ThemeToggle() {
const { theme, toggle } = useTheme()
return <button onClick={toggle}>Switch to {theme === 'light' ? 'dark' : 'light'}</button>
}| Purpose | What it means in practice |
|---|---|
| Provide data & functionality | Share theme, user session, locale, or cart actions anywhere in the subtree |
| Avoid prop drilling | Skip intermediate components that do not use the data |
| Reduce tight coupling | Consumers depend on useTheme(), not on parent prop shapes |
| Centralized state management | One provider owns the source of truth for a concern |
2. Core concepts
Four pieces work together every time you implement the pattern.
| Concept | Role |
|---|---|
| React Context API | Built-in pub/sub for React trees — createContext, Provider, consumer |
| Context boundary | The subtree wrapped by a Provider; only descendants can read the value |
| Provider component | Owns state, fetches data, memoizes value, renders Context.Provider |
| Consumer | Any component that calls useContext or use to read the value |
App
└── ThemeProvider ← Provider component (context boundary starts)
├── Header
│ └── Logo ← inside boundary
└── Sidebar
└── ThemeToggle ← Consumer (useTheme)Context object anatomy
import { createContext } from 'react'
type ThemeContextValue = {
theme: 'light' | 'dark'
toggle: () => void
}
// Default null — forces consumers through a guard hook
const ThemeContext = createContext<ThemeContextValue | null>(null)3. Implementation steps
Follow this five-step recipe for every new Provider.
| Step | Action |
|---|---|
| 1 | Create Context — createContext with a typed default (null for required providers) |
| 2 | Create Provider component — own state, side effects, memoized value |
| 3 | Wrap component hierarchy — place Provider at the lowest common ancestor |
| 4 | Apply custom hook pattern — export useTheme() with a guard |
| 5 | Use data in components — call the hook anywhere inside the boundary |
Step-by-step skeleton
'use client'
import { createContext, useCallback, useContext, useMemo, useState, type ReactNode } from 'react'
// Step 1 — Create Context
type Locale = 'en' | 'bn'
type LocaleContextValue = {
locale: Locale
setLocale: (next: Locale) => void
}
const LocaleContext = createContext<LocaleContextValue | null>(null)
// Step 2 — Provider component
export function LocaleProvider({ children }: { children: ReactNode }) {
const [locale, setLocale] = useState<Locale>('en')
const value = useMemo(
() => ({
locale,
setLocale,
}),
[locale],
)
return <LocaleContext.Provider value={value}>{children}</LocaleContext.Provider>
}
// Step 4 — Custom hook with guard
export function useLocale() {
const ctx = useContext(LocaleContext)
if (!ctx) {
throw new Error('useLocale must be used within <LocaleProvider>')
}
return ctx
}
// Step 3 — Wrap hierarchy (typically in layout or App)
export function AppShell({ children }: { children: ReactNode }) {
return <LocaleProvider>{children}</LocaleProvider>
}
// Step 5 — Consume deep in the tree
function LanguageSwitcher() {
const { locale, setLocale } = useLocale()
return (
<select value={locale} onChange={(e) => setLocale(e.target.value as Locale)}>
<option value="en">English</option>
<option value="bn">বাংলা</option>
</select>
)
}4. React 19 modernizations
React 19 simplifies Context usage in two ways relevant to the Provider pattern.
Context as Provider — shorter JSX
Previously you always wrote <ThemeContext.Provider value={value}>. In React 19, the context object itself can render as the Provider:
'use client'
import { createContext, useMemo, useState, type ReactNode } from 'react'
type Theme = 'light' | 'dark'
const ThemeContext = createContext<{ theme: Theme; toggle: () => void } | null>(null)
export function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setTheme] = useState<Theme>('light')
const value = useMemo(
() => ({
theme,
toggle: () => setTheme((t) => (t === 'light' ? 'dark' : 'light')),
}),
[theme],
)
// React 19 — Context renders as Provider
return <ThemeContext value={value}>{children}</ThemeContext>
}| React 18 | React 19 |
|---|---|
<ThemeContext.Provider value={v}> | <ThemeContext value={v}> |
useContext(ThemeContext) | use(ThemeContext) also supported |
The use API — flexible consumption
React 19's use can read Context and — unlike useContext — may appear inside conditionals when paired with Suspense boundaries for async resources.
'use client'
import { use, createContext, type ReactNode } from 'react'
const BrandContext = createContext<{ name: string; logoUrl: string } | null>(null)
function BrandBadge({ showLogo }: { showLogo: boolean }) {
const brand = use(BrandContext)
if (!brand) return null
return (
<div className="flex items-center gap-2">
{showLogo && <img src={brand.logoUrl} alt="" width={24} height={24} />}
<span>{brand.name}</span>
</div>
)
}5. Example: Theme Provider
The canonical Provider example — light/dark mode with toggle, persisted preference, and document class sync.
'use client'
import { createContext, useCallback, useContext, useEffect, useMemo, useState, type ReactNode } from 'react'
type Theme = 'light' | 'dark'
type ThemeContextValue = {
theme: Theme
toggle: () => void
setTheme: (theme: Theme) => void
}
const ThemeContext = createContext<ThemeContextValue | null>(null)
const STORAGE_KEY = 'app-theme'
function readStoredTheme(): Theme {
if (typeof window === 'undefined') return 'light'
return (localStorage.getItem(STORAGE_KEY) as Theme) ?? 'light'
}
export function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setThemeState] = useState<Theme>(readStoredTheme)
const setTheme = useCallback((next: Theme) => {
setThemeState(next)
localStorage.setItem(STORAGE_KEY, next)
}, [])
const toggle = useCallback(() => {
setTheme(theme === 'light' ? 'dark' : 'light')
}, [theme, setTheme])
useEffect(() => {
document.documentElement.classList.toggle('dark', theme === 'dark')
}, [theme])
const value = useMemo(() => ({ theme, toggle, setTheme }), [theme, toggle, setTheme])
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
}
export function useTheme() {
const ctx = useContext(ThemeContext)
if (!ctx) throw new Error('useTheme must be used within ThemeProvider')
return ctx
}Consumer components
'use client'
import { useTheme } from './ThemeProvider'
function ThemeToggle() {
const { theme, toggle } = useTheme()
return (
<button type="button" aria-label={`Switch to ${theme === 'light' ? 'dark' : 'light'} mode`} onClick={toggle}>
{theme === 'light' ? '🌙' : '☀️'}
</button>
)
}
function SettingsPanel() {
const { theme, setTheme } = useTheme()
return (
<fieldset>
<legend>Appearance</legend>
<label>
<input type="radio" checked={theme === 'light'} onChange={() => setTheme('light')} />
Light
</label>
<label>
<input type="radio" checked={theme === 'dark'} onChange={() => setTheme('dark')} />
Dark
</label>
</fieldset>
)
}6. Example: Brand Provider
White-label apps fetch brand config once and expose name, logo, and colors to the entire tree.
'use client'
import { createContext, useContext, useEffect, useMemo, useState, type ReactNode } from 'react'
type BrandConfig = {
name: string
logoUrl: string
primaryColor: string
}
type BrandContextValue = {
brand: BrandConfig
loading: boolean
error: Error | null
}
const BrandContext = createContext<BrandContextValue | null>(null)
async function fetchBrand(tenantId: string): Promise<BrandConfig> {
const res = await fetch(`/api/tenants/${tenantId}/brand`)
if (!res.ok) throw new Error('Failed to load brand')
return res.json()
}
export function BrandProvider({ tenantId, children }: { tenantId: string; children: ReactNode }) {
const [brand, setBrand] = useState<BrandConfig | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<Error | null>(null)
useEffect(() => {
let cancelled = false
setLoading(true)
setError(null)
fetchBrand(tenantId)
.then((data) => {
if (!cancelled) setBrand(data)
})
.catch((err) => {
if (!cancelled) setError(err instanceof Error ? err : new Error('Unknown error'))
})
.finally(() => {
if (!cancelled) setLoading(false)
})
return () => {
cancelled = true
}
}, [tenantId])
const value = useMemo(
() => ({
brand: brand ?? { name: 'Loading…', logoUrl: '/placeholder.svg', primaryColor: '#000' },
loading,
error,
}),
[brand, loading, error],
)
return <BrandContext.Provider value={value}>{children}</BrandContext.Provider>
}
export function useBrand() {
const ctx = useContext(BrandContext)
if (!ctx) throw new Error('useBrand must be used within BrandProvider')
return ctx
}// Deep header — no tenantId prop drilling
function AppHeader() {
const { brand, loading } = useBrand()
return (
<header style={{ borderColor: brand.primaryColor }}>
<img src={brand.logoUrl} alt={brand.name} width={32} height={32} />
<h1>{loading ? '…' : brand.name}</h1>
</header>
)
}7. Multiple providers (nesting)
Real apps compose several providers — auth, theme, cart, i18n. Nest them rather than merging unrelated state into one mega-context.
'use client'
import type { ReactNode } from 'react'
import { AuthProvider } from './AuthProvider'
import { ThemeProvider } from './ThemeProvider'
import { CartProvider } from './CartProvider'
import { LocaleProvider } from './LocaleProvider'
export function AppProviders({ children }: { children: ReactNode }) {
return (
<AuthProvider>
<LocaleProvider>
<ThemeProvider>
<CartProvider>{children}</CartProvider>
</ThemeProvider>
</LocaleProvider>
</AuthProvider>
)
}Provider composition helper
Reduce pyramid nesting with a small utility:
type ProviderComponent = ({ children }: { children: ReactNode }) => JSX.Element
function composeProviders(...providers: ProviderComponent[]): ProviderComponent {
return ({ children }) => providers.reduceRight((acc, Provider) => <Provider>{acc}</Provider>, children)
}
export const AppProviders = composeProviders(AuthProvider, LocaleProvider, ThemeProvider, CartProvider)| Segregation rule | Why |
|---|---|
| One context per concern | Theme changes do not re-render cart subscribers |
| Order matters for deps | CartProvider may call useAuth() — nest Auth outermost |
| Avoid merged contexts | { theme, user, cart, flags } object re-renders everyone |
8. Common use cases
| Use case | Provider exposes | Typical consumers |
|---|---|---|
| Authentication | user, login, logout, isAuthenticated | Nav, protected routes, profile forms |
| Internationalization | locale, t, setLocale | Copy, date formatters, routing |
| E-commerce cart | items, addItem, removeItem, total | Product cards, mini-cart, checkout |
| Feature flags | isEnabled(flag), flags map | Gated nav items, beta UI sections |
| A/B testing | variant, track(event) | Hero copy, CTA color, experiment hooks |
Authentication sketch
'use client'
import { createContext, useCallback, useContext, useMemo, useState, type ReactNode } from 'react'
type User = { id: string; email: string; role: 'admin' | 'member' }
type AuthContextValue = {
user: User | null
login: (email: string, password: string) => Promise<void>
logout: () => void
}
const AuthContext = createContext<AuthContextValue | null>(null)
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null)
const login = useCallback(async (email: string, password: string) => {
const res = await fetch('/api/auth/login', {
method: 'POST',
body: JSON.stringify({ email, password }),
})
if (!res.ok) throw new Error('Invalid credentials')
setUser(await res.json())
}, [])
const logout = useCallback(() => setUser(null), [])
const value = useMemo(() => ({ user, login, logout }), [user, login, logout])
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>
}
export function useAuth() {
const ctx = useContext(AuthContext)
if (!ctx) throw new Error('useAuth must be used within AuthProvider')
return ctx
}
function AdminLink() {
const { user } = useAuth()
if (user?.role !== 'admin') return null
return <a href="/admin">Admin</a>
}Feature flags sketch
type Flags = Record<string, boolean>
const FeatureFlagContext = createContext<(key: string) => boolean>(() => false)
export function FeatureFlagProvider({ flags, children }: { flags: Flags; children: ReactNode }) {
const isEnabled = useCallback((key: string) => Boolean(flags[key]), [flags])
return <FeatureFlagContext.Provider value={isEnabled}>{children}</FeatureFlagContext.Provider>
}
function NewDashboardBanner() {
const isEnabled = useContext(FeatureFlagContext)
if (!isEnabled('new-dashboard')) return null
return <div className="rounded bg-green-100 p-4">Try the new dashboard!</div>
}9. Best practices & pitfalls
| Practice / pitfall | Guidance |
|---|---|
| Keep context scope narrow | Wrap only the subtree that needs the data — not the entire app by default |
| Segregate data | Separate contexts for theme, auth, cart — never one { everything } object |
| Avoid frequent updates | Do not store mouse position or scroll offset in Context |
| Manage consumer re-renders | Memoize value, split state/actions, memo expensive leaf components |
Re-render mitigation patterns
// ❌ New object every render — all consumers re-render
function BadProvider({ children }: { children: ReactNode }) {
const [count, setCount] = useState(0)
return (
<CountContext.Provider value={{ count, increment: () => setCount((c) => c + 1) }}>{children}</CountContext.Provider>
)
}
// ✅ Stable callbacks + memoized value
function GoodProvider({ children }: { children: ReactNode }) {
const [count, setCount] = useState(0)
const increment = useCallback(() => setCount((c) => c + 1), [])
const value = useMemo(() => ({ count, increment }), [count, increment])
return <CountContext.Provider value={value}>{children}</CountContext.Provider>
}When Context is the wrong tool
| Situation | Better alternative |
|---|---|
| Server-fetched list for one page | Props or TanStack Query in that page |
| High-frequency input (slider) | Local useState or uncontrolled ref |
| Global app with 20+ shared slices | Zustand, Jotai, Redux Toolkit |
| Fire-and-forget events (toast) | Event bus or toast library |
Summary
| Piece | Responsibility |
|---|---|
| Context | Wire format for shared values in a React subtree |
| Provider | Owns state, effects, fetch, memoized value |
| Custom hook | Safe, ergonomic consumer API with guard |
| Boundary | Defines who can read — wrap at lowest common ancestor |
The Provider pattern eliminates prop drilling and centralizes cross-cutting concerns. Implement Theme and Brand providers for the two most common shapes (local state + remote config), nest multiple providers by domain, and split contexts before Context re-renders become a performance problem.
Next reads: Compound Components Pattern for Provider + Context inside widget families, Container-Presenter Pattern for keeping fetch logic out of consumers, and React Design Patterns for the full pattern roadmap.
Subscribe to my newsletter
Stay up to date and get notified when I share new contents.
No spam ever, unsubscribe anytime