State Management: The React Context API

Join the AI Workshop to learn more about AI and how it can be applied to web development. Next cohort February 1st, 2026

The AI-first Web Development BOOTCAMP cohort starts February 24th, 2026. 10 weeks of intensive training and hands-on projects.


The Context API was introduced to allow you to pass state (and enable the state to update) across the app, without having to use props for it.

I wrote this article in 2018, but it was using the old components-based React syntax, something no one uses anymore, so today I finally updated it for hooks.

The React team suggests to stick to props if you have just a few levels of children to pass, because it’s still a much less complicated API than the Context API.

In many cases, it enables us to avoid using Redux, simplifying our apps a lot, and also learning how to use React.

How does it work?

You create a context using React.createContext(), which returns a Context object:

import { createContext } from 'react'

const MyContext = createContext()

The context object contains a Provider component and a default value. In modern React, we typically use the useContext hook instead of the Consumer component.

Then you create a wrapper component that returns a Provider component, and you add as children all the components from which you want to access the context:

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

const MyContext = createContext()

function Container({ children }) {
  const [something, setSomething] = useState('hey')
  
  return (
    <MyContext.Provider value={{ something, setSomething }}>
      {children}
    </MyContext.Provider>
  )
}

function HelloWorld() {
  return (
    <Container>
      <Button />
    </Container>
  )
}

I used Container as the name of this component because this will be a global provider. You can also create smaller contexts.

Inside a component that’s wrapped in a Provider, you use the useContext hook to access the context:

function Button() {
  const { something } = useContext(MyContext)
  
  return <button>{something}</button>
}

You can also pass functions into a Provider value, and those functions will be used by components to update the context state:

function Container({ children }) {
  const [something, setSomething] = useState('hey')
  
  const updateSomething = () => setSomething('ho!')
  
  return (
    <MyContext.Provider value={{ something, updateSomething }}>
      {children}
    </MyContext.Provider>
  )
}

function Button() {
  const { something, updateSomething } = useContext(MyContext)
  
  return (
    <button onClick={updateSomething}>
      {something}
    </button>
  )
}

An Example

Here’s a complete example showing how to use Context API with modern React:

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

// Create the context
const ThemeContext = createContext()

// Provider component
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>
  )
}

// Component that uses the context
function ThemedButton() {
  const { theme, toggleTheme } = useContext(ThemeContext)
  
  return (
    <button 
      onClick={toggleTheme}
      style={{
        backgroundColor: theme === 'light' ? '#fff' : '#333',
        color: theme === 'light' ? '#333' : '#fff'
      }}
    >
      Current theme: {theme}
    </button>
  )
}

// App component
function App() {
  return (
    <ThemeProvider>
      <ThemedButton />
    </ThemeProvider>
  )
}

Multiple Contexts Example

In real applications, you’ll often need multiple contexts to manage different aspects of your application state. Here’s an example showing how to use multiple contexts together:

import { createContext, useContext, useState } 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>
  )
}

// User Context
const UserContext = createContext()
function UserProvider({ children }) {
  const [user, setUser] = useState(null)
  const [isLoading, setIsLoading] = useState(false)
  
  const login = async (credentials) => {
    setIsLoading(true)
    // Simulate API call
    await new Promise(resolve => setTimeout(resolve, 1000))
    setUser({ name: 'John Doe', email: credentials.email })
    setIsLoading(false)
  }
  
  const logout = () => setUser(null)
  
  return (
    <UserContext.Provider value={{ user, login, logout, isLoading }}>
      {children}
    </UserContext.Provider>
  )
}

// Language Context
const LanguageContext = createContext()
function LanguageProvider({ children }) {
  const [language, setLanguage] = useState('en')
  const translations = {
    en: { welcome: 'Welcome', login: 'Login', logout: 'Logout' },
    es: { welcome: 'Bienvenido', login: 'Iniciar sesión', logout: 'Cerrar sesión' }
  }
  
  return (
    <LanguageContext.Provider value={{ language, setLanguage, t: translations[language] }}>
      {children}
    </LanguageContext.Provider>
  )
}

// Component using multiple contexts
function UserProfile() {
  const { theme, toggleTheme } = useContext(ThemeContext)
  const { user, logout, isLoading } = useContext(UserContext)
  const { t, language, setLanguage } = useContext(LanguageContext)
  
  if (isLoading) return <div>Loading...</div>
  
  return (
    <div style={{ 
      padding: '20px', 
      backgroundColor: theme === 'light' ? '#f5f5f5' : '#333',
      color: theme === 'light' ? '#333' : '#fff'
    }}>
      <h2>{t.welcome}, {user?.name || 'Guest'}!</h2>
      
      <div style={{ marginBottom: '10px' }}>
        <button onClick={toggleTheme}>
          Switch to {theme === 'light' ? 'dark' : 'light'} theme
        </button>
      </div>
      
      <div style={{ marginBottom: '10px' }}>
        <button onClick={() => setLanguage(language === 'en' ? 'es' : 'en')}>
          Switch to {language === 'en' ? 'Spanish' : 'English'}
        </button>
      </div>
      
      {user && (
        <button onClick={logout}>
          {t.logout}
        </button>
      )}
    </div>
  )
}

// App with multiple providers
function App() {
  return (
    <ThemeProvider>
      <UserProvider>
        <LanguageProvider>
          <UserProfile />
        </LanguageProvider>
      </UserProvider>
    </ThemeProvider>
  )
}

Why Use Multiple Contexts?

Using multiple contexts is common in real applications for several reasons:

  1. Separation of Concerns: Each context handles a specific domain (theme, user authentication, internationalization, etc.)

  2. Performance: Components only re-render when the specific context they use changes, not when unrelated state updates

  3. Maintainability: It’s easier to manage and debug when each context has a single responsibility

  4. Reusability: Contexts can be composed differently in different parts of your app

  5. Testing: You can test components with only the contexts they need

  6. Team Development: Different developers can work on different contexts without conflicts

The key is to keep each context focused on a single concern and avoid creating one massive context that handles everything in your application.

Lessons in this unit:

0: Introduction
1: Managing state
2: Component props
3: Data flow
4: ▶︎ The React Context API
5: React Concept: Immutability
6: Props vs State in React
7: Unidirectional Data Flow in React
8: Learn how to use Redux
9: The easy-peasy React state management library