π© Why Use Context API?
Before diving in, letβs ground ourselves:
The Context API is Reactβs built-in dependency injection system β it lets us pass data deeply through a tree without prop drilling.
Itβs great for:
- Global themes
- Auth/session data
- Localization (i18n)
- Feature flags
- UI state (modals, drawers)
But it's not a state management library like Redux or Zustand.
π’ Level 1: The Basics β What is Context?
π Creating Context
// theme-context.js
import { createContext } from 'react';
export const ThemeContext = createContext('light');
π¦ Providing Context
// App.jsx
import { ThemeContext } from './theme-context';
function App() {
return (
<ThemeContext.Provider value="dark">
<Layout />
</ThemeContext.Provider>
);
}
π¬ Consuming Context
// Layout.jsx
import { useContext } from 'react';
import { ThemeContext } from './theme-context';
function Layout() {
const theme = useContext(ThemeContext);
return <div className={theme}>Hello</div>;
}
π‘ Level 2: Context with State
Pass a dynamic state (e.g. toggle theme).
// ThemeProvider.jsx
import { createContext, useState, useContext } from 'react';
const ThemeContext = createContext();
export const ThemeProvider = ({ children }) => {
const [theme, setTheme] = useState('light');
const toggle = () => setTheme((t) => (t === 'light' ? 'dark' : 'light'));
return (
<ThemeContext.Provider value={{ theme, toggle }}>
{children}
</ThemeContext.Provider>
);
};
export const useTheme = () => useContext(ThemeContext);
// App.jsx
import { ThemeProvider, useTheme } from './ThemeProvider';
function ThemeToggler() {
const { toggle } = useTheme();
return <button onClick={toggle}>Toggle Theme</button>;
}
function App() {
return (
<ThemeProvider>
<ThemeToggler />
</ThemeProvider>
);
}
β This pattern (provider + custom hook) is scalable and clean.
π Level 3: Performance Gotchas
β Problem
Any component consuming the context will re-render when the context value changes β even if it only uses part of it.
<ThemeContext.Provider value={{ theme, toggle }} />
This object is re-created on every render β causes unnecessary re-renders.
β Solution
Memoize the value:
const value = useMemo(() => ({ theme, toggle }), [theme]);
<ThemeContext.Provider value={value} />
π΅ Level 4: Splitting Context for Performance
If you have a large object:
<Context.Provider value={{ user, setUser, theme, setTheme }} />
Then all consumers of theme
will re-render when user
changes π
β Solution: Split contexts
const UserContext = createContext();
const ThemeContext = createContext();
// Provide separately
<UserContext.Provider value={user}>
<ThemeContext.Provider value={theme}>
<App />
</ThemeContext.Provider>
</UserContext.Provider>
π One responsibility per context.
π΄ Level 5: Composing Context Providers
When your app needs multiple global states (auth, theme, lang), nesting gets ugly.
β
Solution: composeProviders
utility
// ComposeProviders.js
export const composeProviders = (...providers) =>
providers.reduce(
(AccumulatedProviders, CurrentProvider) =>
({ children }) =>
(
<AccumulatedProviders>
<CurrentProvider>{children}</CurrentProvider>
</AccumulatedProviders>
),
({ children }) => <>{children}</>
);
// App.jsx
import { AuthProvider } from './auth';
import { ThemeProvider } from './theme';
import { composeProviders } from './ComposeProviders';
const AppProviders = composeProviders(AuthProvider, ThemeProvider);
function App() {
return (
<AppProviders>
<Main />
</AppProviders>
);
}
β Now your
App.jsx
stays clean and scalable.
π§© Level 6: Context vs Redux/MobX/Zustand
Feature | Context API | Zustand / Redux |
---|---|---|
Built-in | β | β (external) |
Async logic | β (needs hooks) | β |
DevTools support | β | β |
Performance on large state | β (manual split needed) | β |
Boilerplate | Minimal | Medium-High (Redux) |
Use Context API for static or small shared state.
Use state libraries when you need time-travel debugging, middleware, or large-scale updates.
π§ Level 7: Custom Hook Patterns with Context
// ModalContext.js
const ModalContext = createContext();
export const ModalProvider = ({ children }) => {
const [modal, setModal] = useState(null);
const show = (name) => setModal(name);
const hide = () => setModal(null);
const value = useMemo(() => ({ modal, show, hide }), [modal]);
return <ModalContext.Provider value={value}>{children}</ModalContext.Provider>;
};
export const useModal = () => {
const context = useContext(ModalContext);
if (!context) throw new Error('useModal must be inside ModalProvider');
return context;
};
This guards against misuse and provides a clean interface.
βοΈ Advanced Tips
β
Use React.memo
with context consumers
const ThemeButton = React.memo(() => {
const { theme } = useTheme();
return <button className={theme}>Click</button>;
});
β Use selectors (with Zustand or Redux)
But if sticking to Context:
const ThemeValueContext = createContext();
const ThemeActionsContext = createContext();
function ThemeProvider() {
const [theme, setTheme] = useState('light');
return (
<ThemeValueContext.Provider value={theme}>
<ThemeActionsContext.Provider value={{ setTheme }}>
{children}
</ThemeActionsContext.Provider>
</ThemeValueContext.Provider>
);
}
// useThemeValue(), useThemeActions()...
π Final Thought: When to Use Context
β Good for:
- Auth, theme, language
- Configuration objects
- Toggling UI elements
- Cross-cutting app-level state
β Avoid for:
- Frequently changing local UI state
- Large shared state with complex updates
π Summary Cheatsheet
Pattern | Purpose |
---|---|
createContext + useContext |
Basic pattern |
Custom hook (useXyz ) |
Encapsulation & safety |
useMemo on value |
Avoid re-renders |
Split contexts | Isolate state updates |
Compose providers | Cleaner root tree |
Top comments (0)