Best examples of custom context provider in React: 3 core examples (plus more)
Let’s start with the most approachable example of custom context provider in React: a theme provider that toggles between light and dark modes.
You want three things:
- A single source of truth for the current theme
- A way to toggle the theme from any component
- Type safety and good DX (especially in 2024+ React codebases)
Here’s a minimal but production-friendly ThemeProvider using modern React:
// ThemeContext.tsx
import React, { createContext, useContext, useState, useEffect } from 'react';
export type Theme = 'light' | 'dark';
type ThemeContextValue = {
theme: Theme;
toggleTheme: () => void;
};
const ThemeContext = createContext<ThemeContextValue | undefined>(undefined);
export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [theme, setTheme] = useState<Theme>('light');
// Example: respect user's system preference on first load
useEffect(() => {
const prefersDark = window.matchMedia?.('(prefers-color-scheme: dark)').matches;
setTheme(prefersDark ? 'dark' : 'light');
}, []);
const toggleTheme = () => {
setTheme(prev => (prev === 'light' ? 'dark' : 'light'));
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
};
export const useTheme = (): ThemeContextValue => {
const ctx = useContext(ThemeContext);
if (!ctx) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return ctx;
};
And a component using it:
// Header.tsx
import { useTheme } from './ThemeContext';
export function Header() {
const { theme, toggleTheme } = useTheme();
return (
<header className={theme === 'dark' ? 'header-dark' : 'header-light'}>
<h1>My App</h1>
<button onClick={toggleTheme}>
Switch to {theme === 'dark' ? 'light' : 'dark'} mode
</button>
</header>
);
}
This is the cleanest example of how a custom context provider removes prop drilling. Instead of passing theme and toggleTheme through every layout component, any child can call useTheme().
Variations on the theme example
Real examples of custom context provider in React often extend this simple pattern:
- Persist theme to
localStorageso the user’s preference survives reloads - Sync theme with a design system (e.g., Tailwind’s
darkclass or Material UI theme) - Expose more state:
theme,setTheme,systemPreference,isSystemDefault
All of these variations still follow the same ThemeContext provider pattern.
2. AuthContext: examples of custom context provider in React for authentication
Authentication is where custom context providers start to pay off in a big way. You want to:
- Store the current user and auth token
- Expose login/logout methods
- Handle loading and error states
Here’s a realistic example of custom context provider in React that manages authentication state. Imagine you’re hitting an /api/login endpoint that returns a user and a JWT.
// AuthContext.tsx
import React, { createContext, useContext, useEffect, useState } from 'react';
export type User = {
id: string;
email: string;
name: string;
roles: string[];
};
type AuthContextValue = {
user: User | null;
token: string | null;
loading: boolean;
error: string | null;
login: (email: string, password: string) => Promise<void>;
logout: () => void;
};
const AuthContext = createContext<AuthContextValue | undefined>(undefined);
const TOKEN_KEY = 'app_token';
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [user, setUser] = useState<User | null>(null);
const [token, setToken] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
// On first load, try to restore session
useEffect(() => {
const storedToken = localStorage.getItem(TOKEN_KEY);
if (!storedToken) {
setLoading(false);
return;
}
(async () => {
try {
const res = await fetch('/api/me', {
headers: { Authorization: `Bearer ${storedToken}` },
});
if (!res.ok) throw new Error('Session expired');
const data = (await res.json()) as User;
setUser(data);
setToken(storedToken);
} catch (err) {
console.error(err);
localStorage.removeItem(TOKEN_KEY);
} finally {
setLoading(false);
}
})();
}, []);
const login = async (email: string, password: string) => {
setLoading(true);
setError(null);
try {
const res = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
if (!res.ok) throw new Error('Invalid credentials');
const data = (await res.json()) as { token: string; user: User };
setUser(data.user);
setToken(data.token);
localStorage.setItem(TOKEN_KEY, data.token);
} catch (err: any) {
setError(err.message ?? 'Login failed');
} finally {
setLoading(false);
}
};
const logout = () => {
setUser(null);
setToken(null);
localStorage.removeItem(TOKEN_KEY);
};
return (
<AuthContext.Provider value={{ user, token, loading, error, login, logout }}>
{children}
</AuthContext.Provider>
);
};
export const useAuth = (): AuthContextValue => {
const ctx = useContext(AuthContext);
if (!ctx) throw new Error('useAuth must be used within an AuthProvider');
return ctx;
};
Using it in components:
// LoginForm.tsx
import { useAuth } from './AuthContext';
export function LoginForm() {
const { login, loading, error } = useAuth();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const form = e.target as HTMLFormElement;
const email = (form.elements.namedItem('email') as HTMLInputElement).value;
const password = (form.elements.namedItem('password') as HTMLInputElement).value;
await login(email, password);
};
return (
<form onSubmit={handleSubmit}>
<input name="email" type="email" placeholder="Email" required />
<input name="password" type="password" placeholder="Password" required />
<button type="submit" disabled={loading}>
{loading ? 'Signing in…' : 'Sign in'}
</button>
{error && <p style={{ color: 'red' }}>{error}</p>}
</form>
);
}
// ProtectedRoute.tsx
import { Navigate } from 'react-router-dom';
import { useAuth } from './AuthContext';
export function ProtectedRoute({ children }: { children: JSX.Element }) {
const { user, loading } = useAuth();
if (loading) return <div>Loading…</div>;
if (!user) return <Navigate to="/login" replace />;
return children;
}
This is one of the best examples of custom context provider in React because it illustrates the full lifecycle: initialization, async requests, persistence, and consumption. In a modern app, you might also integrate with OpenID Connect or OAuth providers; the context pattern still holds.
If you’re dealing with sensitive health or personal data, you’d combine this approach with guidance on secure session handling from sources like the National Institute of Standards and Technology (NIST) or security recommendations referenced by MedlinePlus and other .gov/.org resources.
3. FeatureFlagContext: examples of custom context provider in React for experiments
Feature flags and A/B tests are everywhere in 2024–2025. Teams ship behind flags, run experiments, and slowly roll out changes. A custom context provider is a clean way to expose flags to your component tree.
Here’s a feature flag example of custom context provider in React:
// FeatureFlagContext.tsx
import React, { createContext, useContext, useEffect, useState } from 'react';
type Flags = Record<string, boolean>;
type FeatureFlagContextValue = {
flags: Flags;
isEnabled: (flagName: string) => boolean;
refreshFlags: () => Promise<void>;
};
const FeatureFlagContext = createContext<FeatureFlagContextValue | undefined>(undefined);
export const FeatureFlagProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [flags, setFlags] = useState<Flags>({});
const fetchFlags = async () => {
const res = await fetch('/api/flags');
if (!res.ok) return;
const data = (await res.json()) as Flags;
setFlags(data);
};
useEffect(() => {
fetchFlags();
}, []);
const isEnabled = (flagName: string) => !!flags[flagName];
const refreshFlags = async () => {
await fetchFlags();
};
return (
<FeatureFlagContext.Provider value={{ flags, isEnabled, refreshFlags }}>
{children}
</FeatureFlagContext.Provider>
);
};
export const useFeatureFlags = (): FeatureFlagContextValue => {
const ctx = useContext(FeatureFlagContext);
if (!ctx) throw new Error('useFeatureFlags must be used within a FeatureFlagProvider');
return ctx;
};
Using it:
// Dashboard.tsx
import { useFeatureFlags } from './FeatureFlagContext';
export function Dashboard() {
const { isEnabled } = useFeatureFlags();
return (
<div>
<h2>Dashboard</h2>
{isEnabled('new-reporting-ui') ? (
<NewReporting />
) : (
<LegacyReporting />
)}
</div>
);
}
This is a practical example of custom context provider in React that maps almost one-to-one with how modern experimentation platforms work. You can extend it with:
- User-specific targeting (e.g., by role or region)
- Flag types beyond boolean (e.g., string variants for A/B/C tests)
- Caching flags per session
In production systems—especially those that touch regulated sectors like healthcare or education—teams often pair this with governance policies and risk guidance from organizations such as the U.S. Department of Health & Human Services or major research institutions like Harvard University.
More real examples of custom context provider in React (beyond the main 3)
The three main patterns above cover a lot of ground, but real apps usually have several more providers. Here are additional examples of custom context provider in React that you’ll see in 2024–2025 codebases:
API Client / Query Context
Instead of importing your API client everywhere, you expose it via context:
// ApiClientContext.tsx
import React, { createContext, useContext } from 'react';
export type ApiClient = {
get: <T>(url: string) => Promise<T>;
post: <T>(url: string, body: unknown) => Promise<T>;
};
const ApiClientContext = createContext<ApiClient | undefined>(undefined);
export const ApiClientProvider: React.FC<{ client: ApiClient; children: React.ReactNode }> = ({
client,
children,
}) => {
return (
<ApiClientContext.Provider value={client}>{children}</ApiClientContext.Provider>
);
};
export const useApiClient = () => {
const ctx = useContext(ApiClientContext);
if (!ctx) throw new Error('useApiClient must be used within an ApiClientProvider');
return ctx;
};
This example of custom context provider in React is especially handy if you’re mocking clients for tests or swapping implementations (e.g., REST vs. GraphQL).
Form Wizard / Stepper Context
Multi-step forms show up everywhere from health intake forms to education portals. A FormWizardContext might expose:
currentStepgoToNext,goToPreviousvalues,setValue
You wrap the entire wizard in a provider and let each step read and write shared state without passing props through every intermediary.
Notification / Toast Context
A NotificationProvider exposes notify({ type, message }) and manages a queue of messages. Any component can trigger a toast without caring where or how it’s rendered.
Settings / Preferences Context
Think user preferences like language, measurement units, or accessibility options (e.g., larger font size, high contrast mode). A SettingsContext lets you:
- Store preferences
- Sync them to
localStorageor an API - Respect OS-level preferences, like reduced motion, which is recommended by accessibility guidelines such as those discussed by educational and health organizations (for instance, accessibility recommendations you’ll find referenced across .gov and .edu resources).
All of these are real examples of custom context provider in React. They follow the same pattern: define the shape of your data, create a context, build a provider, and expose a custom hook.
Patterns and best practices from these 3 core examples
If you look across the three main examples of custom context provider in React above (theme, auth, feature flags), a few patterns repeat:
1. Always export a custom hook
useTheme, useAuth, useFeatureFlags, useApiClient — these hide useContext and give you a single place to handle error messages when the provider is missing.
2. Keep context values stable when possible
For performance, memoize complex values with useMemo or useCallback if your provider does heavy work. That matters when your tree gets large.
3. Avoid putting everything in one mega-provider
In 2024–2025, with large React apps, the better approach is to compose multiple small providers:
// AppProviders.tsx
import { ThemeProvider } from './ThemeContext';
import { AuthProvider } from './AuthContext';
import { FeatureFlagProvider } from './FeatureFlagContext';
export function AppProviders({ children }: { children: React.ReactNode }) {
return (
<AuthProvider>
<ThemeProvider>
<FeatureFlagProvider>{children}</FeatureFlagProvider>
</ThemeProvider>
</AuthProvider>
);
}
This pattern mirrors how many large organizations structure their frontends: clear separation of concerns, each provider responsible for one slice of state.
4. Don’t use context for highly volatile, local state
Context is great for global or app-wide state. For rapidly changing, local UI state (like an input value), React’s own state or libraries like React Query still make more sense. Even the React docs point out that context is not a replacement for every kind of state management; it’s a targeted tool.
FAQ: examples of custom context provider in React
Q1. What are common examples of custom context provider in React used in production apps?
Common examples include AuthProvider for authentication, ThemeProvider for dark/light mode and design system settings, FeatureFlagProvider for experiments, ApiClientProvider for HTTP clients, NotificationProvider for toasts, and SettingsProvider for user preferences like language or accessibility.
Q2. Can you show an example of combining multiple custom context providers?
Yes. The AppProviders component above is a clear example of custom context provider composition. You wrap your root component tree in nested providers, each handling its own domain (auth, theme, flags). Components inside the tree can call useAuth, useTheme, or useFeatureFlags independently.
Q3. Are there performance concerns with these examples of custom context provider in React?
Yes, context re-renders all consumers when its value changes. For low-frequency changes (auth state, theme, feature flags), this is usually fine. For high-frequency updates, consider splitting your context into smaller pieces or pushing volatile state down into local component state. You can also memoize provider values.
Q4. When should I use a custom context provider vs. a state management library?
If your needs match the examples of custom context provider in React shown here—global theme, auth, basic settings—context is often enough. When your app grows into complex data flows with heavy caching, optimistic updates, and server synchronization, libraries like React Query, Redux Toolkit, or Zustand become attractive. Many modern stacks combine both: context for global app wiring, a data library for server state.
Q5. How do these patterns hold up with Server Components and React 18+?
The examples of custom context provider in React above are client-side patterns. In React 18 and beyond, you can still use them in client components. For server components, you’ll often push data fetching to the server and hydrate client-side providers with initial data. The mental model stays the same: context is for shared client state and behaviors.
If you remember nothing else, remember this: the best examples of custom context provider in React all do one simple thing very well. They give you a clean, typed, and centralized way to share state and behavior across your app without drowning in props. Theme, auth, feature flags, API clients, notifications, settings — once you’ve built two or three of these, the pattern becomes second nature.
Related Topics
Explore More React Code Snippets
Discover more examples and insights in this category.
View All React Code Snippets