Skip to main content

Context API Deep Dive: Create and Consume Context

The Context API is React's built-in mechanism for sharing state across deep component hierarchies without prop drilling. This practical guide walks through creating a provider component, using the useContext hook to consume context, and building a real-world theme switcher. You will learn how to structure context for global concerns like theming, user authentication, or language preferences.

Key Takeaways​

  • The Context API allows you to provide global state to any descendant component without passing props down every level
  • Create context with createContext(), then wrap a provider component around children using Context.Provider
  • Consume context in any descendant component with the useContext hook
  • A context provider can pass both state values and update functions, enabling nested components to trigger state changes
  • Theme switchers are a canonical use case for context, allowing any component to access and change the global theme

How Do You Create a Context Provider?​

A Context Provider is a component that manages state and makes it available to all descendants. Here is a theme provider that manages light/dark mode:

import React, { useState, createContext } from 'react';

export const ThemeContext = createContext();

export 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>
);
}

Breakdown:

  • createContext() — Creates a new Context object (exported so other components can subscribe to it).
  • useState('light') — Manages the theme state in the provider.
  • toggleTheme — Function to update the theme; passed in the context value.
  • ThemeContext.Provider — Component that provides the context value to all descendants. Only descendants of the provider can access this context.

How Do You Consume Context with useContext?​

Any descendant component can access the context value using the useContext hook:

import React, { useContext } from 'react';
import { ThemeContext } from './ThemeProvider';

function ThemeSwitcher() {
const { theme, toggleTheme } = useContext(ThemeContext);

return (
<button onClick={toggleTheme}>
Switch to {theme === 'light' ? 'Dark' : 'Light'} Mode
</button>
);
}

export default ThemeSwitcher;

Breakdown:

  • useContext(ThemeContext) — Hook that returns the value object provided by the nearest ThemeContext.Provider ancestor.
  • Destructure theme and toggleTheme from the returned object.
  • Call toggleTheme to update the theme from any nested component.

How Do You Build a Complete Theme Switcher Application?​

Here is a full example that applies the theme to the entire app:

Step 1: Create the ThemeProvider (ThemeProvider.js)

import React, { useState, createContext } from 'react';

export const ThemeContext = createContext();

export 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>
);
}

Step 2: Create a ThemeSwitcher Component (ThemeSwitcher.js)

import React, { useContext } from 'react';
import { ThemeContext } from './ThemeProvider';

function ThemeSwitcher() {
const { theme, toggleTheme } = useContext(ThemeContext);

return (
<button onClick={toggleTheme}>
Switch to {theme === 'light' ? 'Dark' : 'Light'} Mode
</button>
);
}

export default ThemeSwitcher;

Step 3: Wrap the App with the Provider and Apply Theming (App.js)

import React, { useContext } from 'react';
import { ThemeProvider, ThemeContext } from './ThemeProvider';
import ThemeSwitcher from './ThemeSwitcher';
import './App.css';

function AppContent() {
const { theme } = useContext(ThemeContext);

return (
<div className={`App ${theme}`}>
<h1>Theme Switcher</h1>
<ThemeSwitcher />
</div>
);
}

function App() {
return (
<ThemeProvider>
<AppContent />
</ThemeProvider>
);
}

export default App;

Step 4: Add CSS for Theming (App.css)

.App {
padding: 2rem;
transition: background-color 0.3s, color 0.3s;
}

.App.light {
background-color: #ffffff;
color: #333333;
}

.App.dark {
background-color: #333333;
color: #ffffff;
}

button {
padding: 0.5rem 1rem;
border: none;
border-radius: 4px;
cursor: pointer;
}

.App.light button {
background-color: #007bff;
color: white;
}

.App.dark button {
background-color: #0056b3;
color: white;
}

Walkthrough:

  • The ThemeProvider wraps the entire app inside App(), making the theme context available to all descendants.
  • AppContent is a nested component that uses useContext(ThemeContext) to read the theme and apply it to the root div via className.
  • ThemeSwitcher also consumes the context and provides a button to toggle the theme.
  • CSS applies different styles based on the light or dark class.

Best Practices for Context API​

  • One provider per concern: Create separate contexts for separate concerns (theme, auth, user preferences). Do not put everything in one monolithic context.
  • Memoize provider values: If the provider value is computed on every render, wrap it in useMemo to prevent unnecessary re-renders of all consumers.
  • Create custom hooks: Export a custom hook like useTheme() that wraps useContext(ThemeContext) for cleaner consumption.
  • Use context for global, infrequently-changing state: Context works well for themes, auth, language settings. For high-frequency updates (form input, animations), consider local state.
  • Avoid deeply nesting providers: If you have many context providers, consider extracting them into a single provider component to avoid "provider hell."

Frequently Asked Questions​

What is the difference between Context and state management libraries like Redux?​

Context is simpler and built into React; no external library needed. It works well for global, infrequently-changing state (theme, auth). Redux is more powerful for complex state, time-travel debugging, and high-frequency updates. For most apps, Context is sufficient; reach for Redux if Context becomes unwieldy.

Can a context value change after the Provider is created?​

Yes. When state inside the Provider changes (via setTheme), the context value updates, and all consuming components re-render. The Provider itself does not need to be recreated.

What if a component tries to use useContext without a matching Provider ancestor?​

React throws an error. You can provide a default value to createContext(defaultValue), but the default is used only if there is no Provider ancestor. Best practice: always wrap your app with the provider.

How do you prevent unnecessary re-renders of context consumers?​

Wrap the provider's value object in useMemo to stabilize its reference:

const value = useMemo(() => ({ theme, toggleTheme }), [theme]);

Alternatively, split context into separate contexts for state and dispatch to minimize re-render scope.

Can you nest multiple providers of the same context?​

Yes. The innermost Provider takes precedence. Nested providers are useful for overriding context at different component tree levels (e.g., a modal with its own theme override).

Further Reading​