Introduction
The React Context API is a powerful tool for managing global state across a component tree. While basic usage allows sharing static data like a theme or user name, real-world applications often require dynamic updates, multiple contexts, and performance optimizations.
In this post, we will explore:
- Dynamically updating context values
- Combining multiple contexts
- Optimizing context to avoid unnecessary re-renders
- A practical example: managing authentication and user preferences
By mastering these advanced patterns, developers can build scalable, maintainable, and performant React applications.
Updating Context Dynamically
One of the core advantages of Context API is the ability to update context values dynamically in response to user interactions, API responses, or other events.
Dynamic Updates with useState
A common pattern is combining context with useState to allow components to read and modify the shared state.
import React, { createContext, useState, useContext } from 'react';
// Create context
const ThemeContext = createContext();
function ThemeProvider({ children }) {
const [theme, setTheme] = useState("light");
const toggleTheme = () => {
setTheme(prevTheme => (prevTheme === "light" ? "dark" : "light"));
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
function ThemedButton() {
const { theme, toggleTheme } = useContext(ThemeContext);
return (
<button
onClick={toggleTheme}
style={{
backgroundColor: theme === "light" ? "#fff" : "#333",
color: theme === "light" ? "#000" : "#fff",
padding: "10px 20px",
border: "none",
cursor: "pointer"
}}
>
Toggle Theme
</button>
);
}
export { ThemeProvider, ThemedButton };
Explanation:
ThemeProvidermanages the theme state and exposes both the current value and an updater function (toggleTheme) via context.ThemedButtonconsumes the context and dynamically updates the theme.- Any component using
ThemeContextwill automatically re-render when the value changes.
Benefits
- Allows global state to react to user actions.
- Reduces the need for prop drilling across multiple layers.
- Centralizes state logic in the provider.
Combining Multiple Contexts
Complex applications often require different global states, such as theme, authentication, language, or user preferences. Instead of merging unrelated data into a single context, it is best to use multiple contexts for separation of concerns.
Example: Theme + Auth Context
import React, { createContext, useState, useContext } from 'react';
// Theme context
const ThemeContext = createContext();
function ThemeProvider({ children }) {
const [theme, setTheme] = useState("light");
const toggleTheme = () => setTheme(prev => (prev === "light" ? "dark" : "light"));
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
// Auth context
const AuthContext = createContext();
function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const login = (username) => setUser({ name: username });
const logout = () => setUser(null);
return (
<AuthContext.Provider value={{ user, login, logout }}>
{children}
</AuthContext.Provider>
);
}
// App combining multiple providers
function App() {
return (
<AuthProvider>
<ThemeProvider>
<Dashboard />
</ThemeProvider>
</AuthProvider>
);
}
// Dashboard component consuming both contexts
function Dashboard() {
const { user, login, logout } = useContext(AuthContext);
const { theme, toggleTheme } = useContext(ThemeContext);
return (
<div style={{ padding: "20px", backgroundColor: theme === "light" ? "#fff" : "#333", color: theme === "light" ? "#000" : "#fff" }}>
<h1>Welcome {user ? user.name : "Guest"}</h1>
<button onClick={toggleTheme}>Toggle Theme</button>
{user ? (
<button onClick={logout}>Logout</button>
) : (
<button onClick={() => login("Alice")}>Login</button>
)}
</div>
);
}
export default App;
Explanation:
- Separate contexts allow independent management of theme and authentication.
Dashboardconsumes both contexts without prop drilling.- Adding or modifying one context does not affect the other.
Benefits of Multiple Contexts
- Cleaner code with separation of concerns.
- Reduced unnecessary re-renders if providers are independent.
- Easier maintenance and scalability for large applications.
Optimizing Context to Avoid Unnecessary Re-Renders
One common pitfall with context is unintended re-renders. When a context value changes, all consumers re-render, which can impact performance in large apps. Several techniques help optimize context usage:
1. Memoize Context Values
Using useMemo ensures that the context object is only recreated when its dependencies change.
import React, { createContext, useState, useMemo } from 'react';
const ThemeContext = createContext();
function ThemeProvider({ children }) {
const [theme, setTheme] = useState("light");
const toggleTheme = () => setTheme(prev => (prev === "light" ? "dark" : "light"));
const value = useMemo(() => ({ theme, toggleTheme }), [theme]);
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
}
Explanation:
- Without
useMemo,valueis recreated on every render, triggering unnecessary re-renders of consumers. - Memoization prevents re-renders when unrelated state changes occur.
2. Split Contexts by Responsibility
Avoid putting unrelated state into one context. For example, combine theme and authentication in separate providers to minimize unnecessary re-renders:
<AuthProvider>
<ThemeProvider>
<Dashboard />
</ThemeProvider>
</AuthProvider>
Dashboardconsuming only theme context will not re-render when authentication changes.
3. Using Context with useReducer
For complex state logic, using useReducer inside a context provider can improve predictability and performance.
import React, { createContext, useReducer } from 'react';
const AuthContext = createContext();
const initialState = { user: null };
function reducer(state, action) {
switch (action.type) {
case "LOGIN":
return { user: { name: action.payload } };
case "LOGOUT":
return { user: null };
default:
return state;
}
}
function AuthProvider({ children }) {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<AuthContext.Provider value={{ state, dispatch }}>
{children}
</AuthContext.Provider>
);
}
Benefits:
- Centralizes state updates with actions.
- Easier to manage complex updates.
- Works seamlessly with memoization for optimized rendering.
Example: Authentication and User Preferences
Let’s build a practical example combining advanced context features.
Step 1: Create Auth Context
const AuthContext = createContext();
Step 2: Create Preferences Context
const PreferencesContext = createContext();
Step 3: Implement Providers
function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const login = (username) => setUser({ name: username });
const logout = () => setUser(null);
const value = useMemo(() => ({ user, login, logout }), [user]);
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
function PreferencesProvider({ children }) {
const [preferences, setPreferences] = useState({ language: "en", theme: "light" });
const toggleTheme = () => setPreferences(prev => ({ ...prev, theme: prev.theme === "light" ? "dark" : "light" }));
const value = useMemo(() => ({ preferences, toggleTheme }), [preferences]);
return <PreferencesContext.Provider value={value}>{children}</PreferencesContext.Provider>;
}
Step 4: Combine Providers in App
function App() {
return (
<AuthProvider>
<PreferencesProvider>
<Dashboard />
</PreferencesProvider>
</AuthProvider>
);
}
Step 5: Consume Contexts in Dashboard
function Dashboard() {
const { user, login, logout } = useContext(AuthContext);
const { preferences, toggleTheme } = useContext(PreferencesContext);
return (
<div style={{
backgroundColor: preferences.theme === "light" ? "#fff" : "#333",
color: preferences.theme === "light" ? "#000" : "#fff",
padding: "20px"
}}>
<h1>Welcome {user ? user.name : "Guest"}</h1>
<button onClick={toggleTheme}>Toggle Theme</button>
{user ? (
<button onClick={logout}>Logout</button>
) : (
<button onClick={() => login("Alice")}>Login</button>
)}
<p>Language: {preferences.language}</p>
</div>
);
}
Explanation:
AuthContextmanages authentication.PreferencesContextmanages user preferences like theme and language.useMemooptimizes provider values to prevent unnecessary re-renders.
Best Practices for Advanced Context Usage
- Keep Context Focused: One context per responsibility (theme, auth, preferences).
- Use Memoization: Wrap provider values in
useMemoto avoid triggering unnecessary re-renders. - Combine with useReducer: For complex state logic with multiple actions.
- Avoid Overusing Context: Context is meant for global/shared state, not every piece of component state.
- Leverage Multiple Contexts: Combining multiple contexts reduces interdependency and improves performance.
Leave a Reply