Advanced Context API Usage

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:

  • ThemeProvider manages the theme state and exposes both the current value and an updater function (toggleTheme) via context.
  • ThemedButton consumes the context and dynamically updates the theme.
  • Any component using ThemeContext will 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.
  • Dashboard consumes 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, value is 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>
&lt;Dashboard /&gt;
</ThemeProvider> </AuthProvider>
  • Dashboard consuming 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 (
&lt;AuthContext.Provider value={{ state, dispatch }}&gt;
  {children}
&lt;/AuthContext.Provider&gt;
); }

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 (
&lt;AuthProvider&gt;
  &lt;PreferencesProvider&gt;
    &lt;Dashboard /&gt;
  &lt;/PreferencesProvider&gt;
&lt;/AuthProvider&gt;
); }

Step 5: Consume Contexts in Dashboard

function Dashboard() {
  const { user, login, logout } = useContext(AuthContext);
  const { preferences, toggleTheme } = useContext(PreferencesContext);

  return (
&lt;div style={{
  backgroundColor: preferences.theme === "light" ? "#fff" : "#333",
  color: preferences.theme === "light" ? "#000" : "#fff",
  padding: "20px"
}}&gt;
  &lt;h1&gt;Welcome {user ? user.name : "Guest"}&lt;/h1&gt;
  &lt;button onClick={toggleTheme}&gt;Toggle Theme&lt;/button&gt;
  {user ? (
    &lt;button onClick={logout}&gt;Logout&lt;/button&gt;
  ) : (
    &lt;button onClick={() =&gt; login("Alice")}&gt;Login&lt;/button&gt;
  )}
  &lt;p&gt;Language: {preferences.language}&lt;/p&gt;
&lt;/div&gt;
); }

Explanation:

  • AuthContext manages authentication.
  • PreferencesContext manages user preferences like theme and language.
  • useMemo optimizes provider values to prevent unnecessary re-renders.

Best Practices for Advanced Context Usage

  1. Keep Context Focused: One context per responsibility (theme, auth, preferences).
  2. Use Memoization: Wrap provider values in useMemo to avoid triggering unnecessary re-renders.
  3. Combine with useReducer: For complex state logic with multiple actions.
  4. Avoid Overusing Context: Context is meant for global/shared state, not every piece of component state.
  5. Leverage Multiple Contexts: Combining multiple contexts reduces interdependency and improves performance.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *