DEV Community

Cover image for React Context API A New Approach to State Management
Tianya School
Tianya School

Posted on

React Context API A New Approach to State Management

Today, let’s dive into React’s Context API, a powerful tool for sharing state across a component tree without the hassle of prop drilling. For medium-sized projects, the Context API offers a simple and efficient way to manage state without relying on heavy libraries like Redux.

What is the Context API?

The Context API is a built-in React feature that allows you to share data across components without passing props manually through every level of the component tree. Imagine a scenario where your app needs to display user login information in the top navigation, sidebar, and page content. With props, you’d have to pass the data down through every component, creating messy and verbose code. The Context API acts like a “global broadcast station,” letting any component “subscribe” to the data cleanly and efficiently. Its core features include:

  • Shared State: Store data (e.g., user, theme, language) in a Context, accessible to any component.
  • Dynamic Updates: When Context data changes, subscribed components re-render automatically.
  • Simple Integration: Built into React, no external libraries needed.
  • Flexibility: Ideal for lightweight state management and can be combined with Hooks for complex logic.

We’ll start with basic Context usage, then explore Hooks, dynamic updates, performance optimization, and real-world scenarios.

Environment Setup

To use the Context API, set up a React project with Node.js (18.x recommended) and Create React App (or Vite). We’ll use Create React App:

npx create-react-app react-context-demo
cd react-context-demo
npm start
Enter fullscreen mode Exit fullscreen mode

Project structure:

react-context-demo/
├── src/
│   ├── App.js
│   ├── index.js
├── package.json
Enter fullscreen mode Exit fullscreen mode

Run npm start and visit localhost:3000 to see the default page. We’ll write code in src to demonstrate various Context API use cases.

Basic Usage: Creating and Using Context

The Context API revolves around creating a Context object, using a Provider to supply data, and a Consumer or Hooks to access it. Let’s create a simple example to share user data.

Creating a Context

Create src/contexts/UserContext.js:

import { createContext } from 'react';

const UserContext = createContext(null);

export default UserContext;
Enter fullscreen mode Exit fullscreen mode

createContext(null) creates a Context object with null as the default value (used when no Provider is present).

Using Provider to Supply Data

Update src/App.js:

import UserContext from './contexts/UserContext';

function App() {
  const user = { name: 'Alice', role: 'admin' };

  return (
    <UserContext.Provider value={user}>
      <div style={{ padding: 20 }}>
        <h1>Context Demo</h1>
        <UserProfile />
      </div>
    </UserContext.Provider>
  );
}

function UserProfile() {
  return (
    <UserContext.Consumer>
      {user => (
        <div>
          <p>Name: {user.name}</p>
          <p>Role: {user.role}</p>
        </div>
      )}
    </UserContext.Consumer>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Run the app to see:

  • Name: Alice
  • Role: admin

The Provider passes the user object to the Context, and the Consumer in UserProfile retrieves and displays it. The Consumer uses a render function to access the Context value.

Using useContext Hook

The Consumer syntax is verbose; React 16.8+ offers the useContext Hook for a cleaner approach. Update UserProfile in App.js:

import { useContext } from 'react';
import UserContext from './contexts/UserContext';

function App() {
  const user = { name: 'Alice', role: 'admin' };

  return (
    <UserContext.Provider value={user}>
      <div style={{ padding: 20 }}>
        <h1>Context Demo</h1>
        <UserProfile />
      </div>
    </UserContext.Provider>
  );
}

function UserProfile() {
  const user = useContext(UserContext);
  return (
    <div>
      <p>Name: {user.name}</p>
      <p>Role: {user.role}</p>
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

The output is identical, but useContext is more concise, directly returning the Context value, ideal for function components.

Dynamic State: Combining with useState

The Context API often pairs with useState for dynamic state management. Update App.js to add a user-switching feature:

import { useState, useContext } from 'react';
import UserContext from './contexts/UserContext';

function App() {
  const [user, setUser] = useState({ name: 'Alice', role: 'admin' });

  return (
    <UserContext.Provider value={{ user, setUser }}>
      <div style={{ padding: 20 }}>
        <h1>Dynamic Context</h1>
        <UserProfile />
        <UserSwitcher />
      </div>
    </UserContext.Provider>
  );
}

function UserProfile() {
  const { user } = useContext(UserContext);
  return (
    <div>
      <p>Name: {user.name}</p>
      <p>Role: {user.role}</p>
    </div>
  );
}

function UserSwitcher() {
  const { setUser } = useContext(UserContext);
  return (
    <button onClick={() => setUser({ name: 'Bob', role: 'user' })}>
      Switch to Bob
    </button>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Click the button, and the page updates from “Alice, admin” to “Bob, user”. The Provider’s value includes user and setUser, allowing child components to read and update state. When Context data changes, subscribed components re-render automatically.

Nested Components

The Context API shines in deeply nested component trees. Create an example with nested components:

src/components/Dashboard.js:

import { useContext } from 'react';
import UserContext from '../contexts/UserContext';
import Sidebar from './Sidebar';
import Content from './Content';

function Dashboard() {
  return (
    <div style={{ display: 'flex', gap: '20px' }}>
      <Sidebar />
      <Content />
    </div>
  );
}

export default Dashboard;
Enter fullscreen mode Exit fullscreen mode

src/components/Sidebar.js:

import { useContext } from 'react';
import UserContext from '../contexts/UserContext';

function Sidebar() {
  const { user } = useContext(UserContext);
  return (
    <div style={{ border: '1px solid', padding: 10 }}>
      <h2>Sidebar</h2>
      <p>Welcome, {user.name}!</p>
    </div>
  );
}

export default Sidebar;
Enter fullscreen mode Exit fullscreen mode

src/components/Content.js:

import { useContext } from 'react';
import UserContext from '../contexts/UserContext';
import ProfileCard from './ProfileCard';

function Content() {
  return (
    <div style={{ padding: 10 }}>
      <h2>Content</h2>
      <ProfileCard />
    </div>
  );
}

export default Content;
Enter fullscreen mode Exit fullscreen mode

src/components/ProfileCard.js:

import { useContext } from 'react';
import UserContext from '../contexts/UserContext';

function ProfileCard() {
  const { user } = useContext(UserContext);
  return (
    <div style={{ border: '1px solid', padding: 10 }}>
      <p>Name: {user.name}</p>
      <p>Role: {user.role}</p>
    </div>
  );
}

export default ProfileCard;
Enter fullscreen mode Exit fullscreen mode

Update App.js:

import { useState } from 'react';
import UserContext from './contexts/UserContext';
import Dashboard from './components/Dashboard';

function App() {
  const [user, setUser] = useState({ name: 'Alice', role: 'admin' });

  return (
    <UserContext.Provider value={{ user, setUser }}>
      <div style={{ padding: 20 }}>
        <h1>Nested Components</h1>
        <Dashboard />
        <button onClick={() => setUser({ name: 'Bob', role: 'user' })}>
          Switch to Bob
        </button>
      </div>
    </UserContext.Provider>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Run the app. Sidebar and ProfileCard (nested in Content) both access user. Clicking the button updates all components synchronously. The Context API eliminates prop drilling, keeping the code clean.

Multiple Contexts: Managing Different States

Real projects often require multiple Contexts, such as for user, theme, or language. Create a theme-switching example:

src/contexts/ThemeContext.js:

import { createContext } from 'react';

const ThemeContext = createContext('light');

export default ThemeContext;
Enter fullscreen mode Exit fullscreen mode

Update App.js:

import { useState } from 'react';
import UserContext from './contexts/UserContext';
import ThemeContext from './contexts/ThemeContext';
import Dashboard from './components/Dashboard';

function App() {
  const [user, setUser] = useState({ name: 'Alice', role: 'admin' });
  const [theme, setTheme] = useState('light');

  return (
    <UserContext.Provider value={{ user, setUser }}>
      <ThemeContext.Provider value={{ theme, setTheme }}>
        <div style={{ padding: 20, background: theme === 'light' ? '#fff' : '#333', color: theme === 'light' ? '#000' : '#fff' }}>
          <h1>Multiple Contexts</h1>
          <Dashboard />
          <button onClick={() => setUser({ name: 'Bob', role: 'user' })}>
            Switch to Bob
          </button>
          <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
            Toggle Theme
          </button>
        </div>
      </ThemeContext.Provider>
    </UserContext.Provider>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Update src/components/Sidebar.js:

import { useContext } from 'react';
import UserContext from '../contexts/UserContext';
import ThemeContext from '../contexts/ThemeContext';

function Sidebar() {
  const { user } = useContext(UserContext);
  const { theme } = useContext(ThemeContext);
  return (
    <div style={{ border: '1px solid', padding: 10, background: theme === 'light' ? '#f0f0f0' : '#444' }}>
      <h2>Sidebar</h2>
      <p>Welcome, {user.name}!</p>
      <p>Theme: {theme}</p>
    </div>
  );
}

export default Sidebar;
Enter fullscreen mode Exit fullscreen mode

Update Content and ProfileCard similarly to access UserContext and ThemeContext. Clicking “Toggle Theme” switches between light and dark themes, while user and theme are managed independently.

Combining with useReducer: Complex State Logic

For complex state logic, combine useReducer with Context for clearer management. Create a counter Context:

src/contexts/CounterContext.js:

import { createContext } from 'react';

const CounterContext = createContext(null);

export default CounterContext;
Enter fullscreen mode Exit fullscreen mode

src/reducers/counterReducer.js:

function counterReducer(state, action) {
  switch (action.type) {
    case 'INCREMENT':
      return { count: state.count + 1 };
    case 'DECREMENT':
      return { count: state.count - 1 };
    case 'RESET':
      return { count: 0 };
    default:
      return state;
  }
}

export default counterReducer;
Enter fullscreen mode Exit fullscreen mode

Update App.js:

import { useReducer } from 'react';
import CounterContext from './contexts/CounterContext';
import counterReducer from './reducers/counterReducer';
import CounterDisplay from './components/CounterDisplay';
import CounterControls from './components/CounterControls';

function App() {
  const [state, dispatch] = useReducer(counterReducer, { count: 0 });

  return (
    <CounterContext.Provider value={{ state, dispatch }}>
      <div style={{ padding: 20 }}>
        <h1>Counter with useReducer</h1>
        <CounterDisplay />
        <CounterControls />
      </div>
    </CounterContext.Provider>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

src/components/CounterDisplay.js:

import { useContext } from 'react';
import CounterContext from '../contexts/CounterContext';

function CounterDisplay() {
  const { state } = useContext(CounterContext);
  return <p>Count: {state.count}</p>;
}

export default CounterDisplay;
Enter fullscreen mode Exit fullscreen mode

src/components/CounterControls.js:

import { useContext } from 'react';
import CounterContext from '../contexts/CounterContext';

function CounterControls() {
  const { dispatch } = useContext(CounterContext);
  return (
    <div>
      <button onClick={() => dispatch({ type: 'INCREMENT' })}>Increment</button>
      <button onClick={() => dispatch({ type: 'DECREMENT' })}>Decrement</button>
      <button onClick={() => dispatch({ type: 'RESET' })}>Reset</button>
    </div>
  );
}

export default CounterControls;
Enter fullscreen mode Exit fullscreen mode

Run the app. Clicking buttons updates the counter. useReducer centralizes state logic, and Context shares dispatch and state across the component tree, ideal for complex state management.

Performance Optimization: Avoiding Unnecessary Re-renders

When Context data changes, all subscribed components re-render, which can impact performance. Use useMemo and split Contexts to optimize.

Update App.js:

import { useState, useMemo } from 'react';
import UserContext from './contexts/UserContext';
import ThemeContext from './contexts/ThemeContext';
import Dashboard from './components/Dashboard';

function App() {
  const [user, setUser] = useState({ name: 'Alice', role: 'admin' });
  const [theme, setTheme] = useState('light');

  const userValue = useMemo(() => ({ user, setUser }), [user]);
  const themeValue = useMemo(() => ({ theme, setTheme }), [theme]);

  return (
    <UserContext.Provider value={userValue}>
      <ThemeContext.Provider value={themeValue}>
        <div style={{ padding: 20, background: theme === 'light' ? '#fff' : '#333', color: theme === 'light' ? '#000' : '#fff' }}>
          <h1>Optimized Context</h1>
          <Dashboard />
          <button onClick={() => setUser({ name: 'Bob', role: 'user' })}>
            Switch to Bob
          </button>
          <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
            Toggle Theme
          </button>
        </div>
      </ThemeContext.Provider>
    </UserContext.Provider>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

useMemo ensures userValue and themeValue only update when their dependencies change, reducing unnecessary Provider re-renders. Splitting UserContext and ThemeContext allows components to subscribe only to needed Contexts.

Real-World Scenario: E-commerce Application

Create an e-commerce app with user login, product list, and cart, using Context for state management.

src/contexts/CartContext.js:

import { createContext } from 'react';

const CartContext = createContext(null);

export default CartContext;
Enter fullscreen mode Exit fullscreen mode

src/App.js:

import { useState } from 'react';
import UserContext from './contexts/UserContext';
import CartContext from './contexts/CartContext';
import Header from './components/Header';
import ProductList from './components/ProductList';
import Cart from './components/Cart';

function App() {
  const [user, setUser] = useState({ name: 'Alice', role: 'customer' });
  const [cart, setCart] = useState([]);

  const addToCart = (product) => {
    setCart([...cart, product]);
  };

  const removeFromCart = (id) => {
    setCart(cart.filter(item => item.id !== id));
  };

  return (
    <UserContext.Provider value={{ user, setUser }}>
      <CartContext.Provider value={{ cart, addToCart, removeFromCart }}>
        <div style={{ padding: 20 }}>
          <Header />
          <ProductList />
          <Cart />
        </div>
      </CartContext.Provider>
    </UserContext.Provider>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

src/components/Header.js:

import { useContext } from 'react';
import UserContext from '../contexts/UserContext';
import CartContext from '../contexts/CartContext';

function Header() {
  const { user, setUser } = useContext(UserContext);
  const { cart } = useContext(CartContext);

  return (
    <header style={{ borderBottom: '1px solid', padding: 10 }}>
      <h1>Store</h1>
      <p>Welcome, {user.name}! Cart items: {cart.length}</p>
      <button onClick={() => setUser({ name: 'Bob', role: 'customer' })}>
        Switch to Bob
      </button>
    </header>
  );
}

export default Header;
Enter fullscreen mode Exit fullscreen mode

src/components/ProductList.js:

import { useContext } from 'react';
import CartContext from '../contexts/CartContext';

const products = [
  { id: 1, name: 'Laptop', price: 999 },
  { id: 2, name: 'Phone', price: 499 },
];

function ProductList() {
  const { addToCart } = useContext(CartContext);

  return (
    <div style={{ padding: 10 }}>
      <h2>Products</h2>
      <ul>
        {products.map(product => (
          <li key={product.id}>
            {product.name} - ${product.price}
            <button onClick={() => addToCart(product)}>Add to Cart</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

export default ProductList;
Enter fullscreen mode Exit fullscreen mode

src/components/Cart.js:

import { useContext } from 'react';
import CartContext from '../contexts/CartContext';

function Cart() {
  const { cart, removeFromCart } = useContext(CartContext);

  return (
    <div style={{ padding: 10 }}>
      <h2>Cart</h2>
      {cart.length === 0 ? (
        <p>Cart is empty</p>
      ) : (
        <ul>
          {cart.map(item => (
            <li key={item.id}>
              {item.name} - ${item.price}
              <button onClick={() => removeFromCart(item.id)}>Remove</button>
            </li>
          ))}
        </ul>
      )}
    </div>
  );
}

export default Cart;
Enter fullscreen mode Exit fullscreen mode

Run the app. Clicking “Add to Cart” adds products, Header shows the cart count, and Cart lists items. UserContext manages user data, and CartContext handles the cart, enabling smooth state sharing across components.

Async Data: Combining with API Requests

Use Context to manage asynchronous data, such as fetching products from an API. Install axios:

npm install axios
Enter fullscreen mode Exit fullscreen mode

src/contexts/ProductContext.js:

import { createContext } from 'react';

const ProductContext = createContext(null);

export default ProductContext;
Enter fullscreen mode Exit fullscreen mode

Update App.js:

import { useState, useEffect } from 'react';
import axios from 'axios';
import ProductContext from './contexts/ProductContext';
import ProductList from './components/ProductList';

function App() {
  const [products, setProducts] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    axios.get('https://jsonplaceholder.typicode.com/posts')
      .then(response => {
        setProducts(response.data.slice(0, 5));
        setLoading(false);
      });
  }, []);

  return (
    <ProductContext.Provider value={{ products, loading }}>
      <div style={{ padding: 20 }}>
        <h1>Async Products</h1>
        <ProductList />
      </div>
    </ProductContext.Provider>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

src/components/ProductList.js:

import { useContext } from 'react';
import ProductContext from '../contexts/ProductContext';

function ProductList() {
  const { products, loading } = useContext(ProductContext);

  if (loading) return <p>Loading...</p>;

  return (
    <ul>
      {products.map(product => (
        <li key={product.id}>{product.title}</li>
      ))}
    </ul>
  );
}

export default ProductList;
Enter fullscreen mode Exit fullscreen mode

Run the app to display post titles from the API after loading. ProductContext manages the async state, and ProductList consumes it directly.

Conclusion (Technical Details)

The React Context API is a lightweight solution for sharing global state, perfect for avoiding prop drilling. The examples demonstrated:

  • Creating and using Context (Provider, Consumer, useContext).
  • Dynamic state management with useState.
  • Sharing data in nested components.
  • Managing multiple Contexts (user, theme).
  • Handling complex logic with useReducer.
  • Optimizing re-renders with useMemo.
  • E-commerce and async data scenarios.

Run these examples, experiment with user switching, theme toggling, and cart updates, and experience the flexibility of the Context API!

Top comments (0)