What is the Context API, and how do you use it to manage state?

The Context API is a built-in feature in React that provides a way to share data between components without having to explicitly pass props through every level of the component tree. It’s designed to solve the problem of “prop drilling” - passing props through intermediate components that don’t need the data but only serve as a means to pass it down.

Core Components of Context API

The Context API consists of three main parts:

  1. React.createContext: Creates a Context object
  2. Context.Provider: Provides the context value to components
  3. Context.Consumer or useContext hook: Consumes the context value

Creating and Using Context

Step 1: Create a Context

import React, { createContext } from 'react';

// Create a context with a default value
const ThemeContext = createContext('light');

// Export the context for use in other components
export default ThemeContext;

Step 2: Provide the Context

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

function App() {
  const [theme, setTheme] = useState('light');
  
  return (
    <ThemeContext.Provider value={theme}>
      <div className={`app ${theme}`}>
        <Header />
        <MainContent />
        <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
          Toggle Theme
        </button>
      </div>
    </ThemeContext.Provider>
  );
}

Step 3: Consume the Context

Using useContext Hook (Functional Components)

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

function ThemedButton() {
  // Get the current theme value from context
  const theme = useContext(ThemeContext);
  
  return (
    <button className={`button-${theme}`}>
      I'm styled based on the theme!
    </button>
  );
}

Using Context.Consumer (Class or Functional Components)

import React from 'react';
import ThemeContext from './ThemeContext';

function ThemedButton() {
  return (
    <ThemeContext.Consumer>
      {theme => (
        <button className={`button-${theme}`}>
          I'm styled based on the theme!
        </button>
      )}
    </ThemeContext.Consumer>
  );
}

Using contextType (Class Components Only)

import React from 'react';
import ThemeContext from './ThemeContext';

class ThemedButton extends React.Component {
  // Set the context type
  static contextType = ThemeContext;
  
  render() {
    const theme = this.context;
    
    return (
      <button className={`button-${theme}`}>
        I'm styled based on the theme!
      </button>
    );
  }
}

Managing State with Context

While Context itself is just a mechanism for passing data, it’s commonly used with React’s state management to create a simple state management solution.

Basic State Management Pattern

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

// Step 1: Create context with a default value
const CounterContext = createContext({
  count: 0,
  increment: () => {},
  decrement: () => {}
});

// Step 2: Create a provider component
export function CounterProvider({ children }) {
  const [count, setCount] = useState(0);
  
  const increment = () => setCount(prevCount => prevCount + 1);
  const decrement = () => setCount(prevCount => prevCount - 1);
  
  // Create the value object that will be provided
  const value = {
    count,
    increment,
    decrement
  };
  
  return (
    <CounterContext.Provider value={value}>
      {children}
    </CounterContext.Provider>
  );
}

// Step 3: Create a custom hook for consuming the context
export function useCounter() {
  const context = useContext(CounterContext);
  
  if (context === undefined) {
    throw new Error('useCounter must be used within a CounterProvider');
  }
  
  return context;
}

// Step 4: Use the provider at the top level
function App() {
  return (
    <CounterProvider>
      <div className="app">
        <Counter />
        <OtherComponent />
      </div>
    </CounterProvider>
  );
}

// Step 5: Consume the context in components
function Counter() {
  const { count, increment, decrement } = useCounter();
  
  return (
    <div>
      <h2>Count: {count}</h2>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
    </div>
  );
}

function OtherComponent() {
  const { count } = useCounter();
  
  return (
    <div>
      <p>The current count is: {count}</p>
    </div>
  );
}

Using Context with useReducer

For more complex state logic, you can combine Context with useReducer:

import React, { createContext, useReducer, useContext } from 'react';

// Step 1: Define the initial state
const initialState = {
  user: null,
  isAuthenticated: false,
  isLoading: false,
  error: null
};

// Step 2: Create a reducer function
function authReducer(state, action) {
  switch (action.type) {
    case 'LOGIN_REQUEST':
      return {
        ...state,
        isLoading: true,
        error: null
      };
    case 'LOGIN_SUCCESS':
      return {
        ...state,
        isLoading: false,
        isAuthenticated: true,
        user: action.payload,
        error: null
      };
    case 'LOGIN_FAILURE':
      return {
        ...state,
        isLoading: false,
        isAuthenticated: false,
        user: null,
        error: action.payload
      };
    case 'LOGOUT':
      return {
        ...state,
        isAuthenticated: false,
        user: null
      };
    default:
      return state;
  }
}

// Step 3: Create the context
const AuthContext = createContext();

// Step 4: Create a provider component
export function AuthProvider({ children }) {
  const [state, dispatch] = useReducer(authReducer, initialState);
  
  // Define actions
  const login = async (credentials) => {
    try {
      dispatch({ type: 'LOGIN_REQUEST' });
      
      // Simulate API call
      const response = await fakeAuthApi.login(credentials);
      
      dispatch({ 
        type: 'LOGIN_SUCCESS', 
        payload: response.user 
      });
    } catch (error) {
      dispatch({ 
        type: 'LOGIN_FAILURE', 
        payload: error.message 
      });
    }
  };
  
  const logout = () => {
    dispatch({ type: 'LOGOUT' });
  };
  
  // Create value object with state and actions
  const value = {
    ...state,
    login,
    logout
  };
  
  return (
    <AuthContext.Provider value={value}>
      {children}
    </AuthContext.Provider>
  );
}

// Step 5: Create a custom hook for consuming the context
export function useAuth() {
  const context = useContext(AuthContext);
  
  if (context === undefined) {
    throw new Error('useAuth must be used within an AuthProvider');
  }
  
  return context;
}

// Fake auth API for the example
const fakeAuthApi = {
  login: async (credentials) => {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        if (credentials.username === 'user' && credentials.password === 'pass') {
          resolve({
            user: {
              id: 1,
              username: 'user',
              name: 'Test User'
            }
          });
        } else {
          reject(new Error('Invalid credentials'));
        }
      }, 1000);
    });
  }
};

Multiple Contexts

You can use multiple contexts in your application for different concerns:

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

Context API Best Practices

1. Split contexts by domain

Create separate contexts for different domains of your application:

// Good: Separate contexts
const UserContext = createContext();
const ThemeContext = createContext();
const CartContext = createContext();

// Avoid: One giant context
const AppContext = createContext();

2. Provide default values

Always provide meaningful default values for your contexts:

// With default value
const ThemeContext = createContext({ theme: 'light', toggleTheme: () => {} });

// Without default value (less ideal)
const ThemeContext = createContext();

3. Create custom hooks

Create custom hooks to consume your contexts:

function useTheme() {
  const context = useContext(ThemeContext);
  if (context === undefined) {
    throw new Error('useTheme must be used within a ThemeProvider');
  }
  return context;
}

4. Optimize renders

Use React.memo and careful context structure to prevent unnecessary re-renders:

// Split state and dispatch into separate contexts
const CountStateContext = createContext();
const CountDispatchContext = createContext();

function CountProvider({ children }) {
  const [count, dispatch] = useReducer(countReducer, 0);
  
  return (
    <CountStateContext.Provider value={count}>
      <CountDispatchContext.Provider value={dispatch}>
        {children}
      </CountDispatchContext.Provider>
    </CountStateContext.Provider>
  );
}

5. Keep provider components close to where they’re needed

Don’t always put all providers at the root level:

function App() {
  return (
    <AuthProvider>
      <Router>
        <Route path="/admin">
          {/* AdminProvider only used in admin section */}
          <AdminProvider>
            <AdminDashboard />
          </AdminProvider>
        </Route>
        <Route path="/shop">
          {/* CartProvider only used in shop section */}
          <CartProvider>
            <Shop />
          </CartProvider>
        </Route>
      </Router>
    </AuthProvider>
  );
}

Context API vs. Other State Management Solutions

FeatureContext APIReduxMobX
Learning curveLowMedium-HighMedium
BoilerplateMinimalSignificantMinimal
Debugging toolsLimitedExcellentGood
PerformanceGood for small-medium appsExcellent for large appsGood
Middleware supportManual implementationBuilt-inManual implementation
Time travel debuggingNoYesNo
Best suited forSmall-medium apps, component-specific stateLarge apps, complex state logicMedium-large apps

Interview Tips

  1. Explain the purpose: Context API solves the problem of prop drilling by providing a way to share values between components without passing props through every level.

  2. Describe the components: Be ready to explain the three main parts: createContext, Provider, and Consumer/useContext.

  3. Compare with prop drilling: Highlight the benefits of Context over passing props through multiple levels of components.

  4. Discuss performance implications: Mention that Context is not optimized for high-frequency updates and may cause re-renders in all consuming components.

  5. Compare with other state management: Be prepared to compare Context with Redux, MobX, or other state management libraries.

  6. Mention best practices: Discuss splitting contexts by domain, providing default values, and creating custom hooks.

  7. Real-world examples: Share examples of how you’ve used Context API in your projects, such as for theme switching, authentication, or shopping carts.

  8. Limitations: Acknowledge that Context is not a complete state management solution and may not be suitable for all use cases, especially those requiring high-performance updates.

Test Your Knowledge

Take a quick quiz to test your understanding of this topic.

Test Your React Knowledge

Ready to put your skills to the test? Take our interactive React quiz and get instant feedback on your answers.