The Context API is React’s built-in solution for state management and dependency injection that avoids prop drilling. Let’s explore its advanced capabilities, patterns, and best practices.
Core Concepts Revisited
1. Basic Context Structure
const ThemeContext = createContext('light'); // Default value
function App() {
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
function Toolbar() {
return <ThemedButton />;
}
function ThemedButton() {
const theme = useContext(ThemeContext);
return <button className={theme}>I'm {theme}!</button>;
}
Advanced Patterns
1. Multi-Context Composition
const UserContext = createContext(null);
const PermissionsContext = createContext([]);
function App() {
const [user, setUser] = useState({ id: 1, name: 'John' });
const [permissions, setPermissions] = useState(['read', 'write']);
return (
<UserContext.Provider value={{ user, setUser }}>
<PermissionsContext.Provider value={{ permissions, setPermissions }}>
<Dashboard />
</PermissionsContext.Provider>
</UserContext.Provider>
);
}
function Dashboard() {
return (
<>
<UserProfile />
<SecuritySettings />
</>
);
}
2. Dynamic Context Providers
function DynamicProviders({ providers, children }) {
return providers.reduceRight((child, Provider) => {
return <Provider>{child}</Provider>;
}, children);
}
// Usage
<DynamicProviders providers={[
<UserContext.Provider value={userData} />,
<ThemeContext.Provider value={theme} />,
<FeaturesContext.Provider value={features} />
]}>
<App />
</DynamicProviders>
3. Context Selectors (Performance Optimization)
const UserContext = createContext();
function UserProvider({ children }) {
const [state, setState] = useState({
user: null,
preferences: {},
isLoading: false
});
// Memoize context value
const contextValue = useMemo(() => ({
state,
setState,
login: (user) => setState(prev => ({ ...prev, user })
}), [state]);
return (
<UserContext.Provider value={contextValue}>
{children}
</UserContext.Provider>
);
}
// Custom hook with selector
function useUser(selector = state => state) {
const { state } = useContext(UserContext);
return selector(state);
}
// Usage - only re-renders when user changes
function UserAvatar() {
const user = useUser(state => state.user);
return <img src={user?.avatar} />;
}
Performance Optimization Techniques
1. Memoization Patterns
const SettingsContext = createContext();
function SettingsProvider({ children }) {
const [settings, setSettings] = useState({ theme: 'dark', notifications: true });
// Important: Memoize the context value to prevent unnecessary re-renders
const value = useMemo(() => ({ settings, setSettings }), [settings]);
return (
<SettingsContext.Provider value={value}>
{children}
</SettingsContext.Provider>
);
}
2. Component Memoization
const ThemedButton = memo(function ThemedButton() {
const theme = useContext(ThemeContext);
return <button className={theme}>Themed Button</button>;
});
3. Context Partitioning
// Instead of one large context
const AppContext = createContext();
// Split into smaller contexts
const UserContext = createContext();
const UIContext = createContext();
const DataContext = createContext();
Advanced Use Cases
1. Generic Context Factory
function createContextStore(initialState, reducer) {
const StateContext = createContext();
const DispatchContext = createContext();
function Provider({ children }) {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<StateContext.Provider value={state}>
<DispatchContext.Provider value={dispatch}>
{children}
</DispatchContext.Provider>
</StateContext.Provider>
);
}
function useStateContext(selector = state => state) {
const state = useContext(StateContext);
return selector(state);
}
function useDispatchContext() {
return useContext(DispatchContext);
}
return [Provider, useStateContext, useDispatchContext];
}
// Usage
const [UserProvider, useUser, useUserDispatch] = createContextStore(
{ name: '', age: 0 },
(state, action) => {
switch (action.type) {
case 'UPDATE_NAME': return { ...state, name: action.payload };
case 'UPDATE_AGE': return { ...state, age: action.payload };
default: return state;
}
}
);
2. Scoped Context Instances
function createScopedContext(defaultValue) {
const Context = createContext(defaultValue);
function Provider({ value, children, scope }) {
const parentValue = useContext(Context);
const scopedValue = scope ? value : parentValue;
return (
<Context.Provider value={scopedValue}>
{children}
</Context.Provider>
);
}
return [Provider, Context];
}
// Usage
const [ThemeProvider, ThemeContext] = createScopedContext('light');
function App() {
return (
<ThemeProvider value="dark">
<Component /> {/* Gets dark theme */}
<ThemeProvider value="blue" scope>
<Component /> {/* Gets blue theme */}
</ThemeProvider>
</ThemeProvider>
);
}
3. Context with Middleware
function createEnhancedContext(defaultValue, middleware) {
const Context = createContext(defaultValue);
function Provider({ value, children }) {
const enhancedValue = useMemo(() => {
return middleware ? middleware(value) : value;
}, [value]);
return (
<Context.Provider value={enhancedValue}>
{children}
</Context.Provider>
);
}
return [Provider, Context];
}
// Usage
const loggerMiddleware = (contextValue) => {
console.log('Context value:', contextValue);
return contextValue;
};
const [UserProvider, UserContext] = createEnhancedContext(null, loggerMiddleware);
TypeScript Integration
1. Strongly Typed Context
interface UserContextType {
user: User | null;
login: (email: string, password: string) => Promise<void>;
logout: () => void;
}
const UserContext = createContext<UserContextType | undefined>(undefined);
function UserProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const login = async (email: string, password: string) => {
// Login logic
};
const logout = () => setUser(null);
return (
<UserContext.Provider value={{ user, login, logout }}>
{children}
</UserContext.Provider>
);
}
function useUser() {
const context = useContext(UserContext);
if (context === undefined) {
throw new Error('useUser must be used within a UserProvider');
}
return context;
}
2. Generic Context Creator
function createTypedContext<T>() {
const Context = createContext<T | undefined>(undefined);
const useTypedContext = () => {
const context = useContext(Context);
if (context === undefined) {
throw new Error('useTypedContext must be used within its Provider');
}
return context;
};
return [Context.Provider, useTypedContext] as const;
}
// Usage
interface ThemeContextType {
theme: string;
toggleTheme: () => void;
}
const [ThemeProvider, useTheme] = createTypedContext<ThemeContextType>();
Best Practices
- Provider Composition: Place providers close to where they’re needed
- Memoization: Always memoize context values that include objects/functions
- Sensible Defaults: Provide meaningful default values when appropriate
- Error Handling: Throw helpful errors when context is used outside provider
- Documentation: Document the expected shape of context values
- Testing: Test components with mock providers
Testing Strategies
1. Custom Test Providers
function renderWithProviders(ui, { providerProps, ...renderOptions }) {
return render(
<UserContext.Provider {...providerProps}>
<ThemeContext.Provider {...providerProps}>
{ui}
</ThemeContext.Provider>
</UserContext.Provider>,
renderOptions
);
}
// In tests
test('should render with context', () => {
const providerProps = {
value: { user: mockUser, login: jest.fn() }
};
const { getByText } = renderWithProviders(<UserProfile />, { providerProps });
expect(getByText(mockUser.name)).toBeInTheDocument();
});
2. Mocking Context
jest.mock('../contexts/UserContext', () => ({
useUser: () => ({
user: mockUser,
login: jest.fn(),
logout: jest.fn()
})
}));
test('should display user name', () => {
render(<UserProfile />);
expect(screen.getByText(mockUser.name)).toBeInTheDocument();
});
Common Pitfalls
- Unnecessary Renders: Forgetting to memoize context values
- Provider Order: Nesting providers in wrong order
- Default Values: Using defaults when provider is expected
- Memory Leaks: Not cleaning up subscriptions in effects
- Overuse: Using context for state that should be local
The Context API is a powerful tool when used properly. By understanding these advanced patterns and performance considerations, you can build more maintainable and efficient React applications.