React useContext: Building a Light/Dark Theme Toggle

Learning React


To better understand useContext, you need to be familiar with the following concepts of React:

  • Components
  • UseState
  • React Router DOM (Optional, but useful)

React's Context API solves a specific problem: sharing state between components without passing props through every level of your component tree.

When you need to pass data through multiple component layers, you end up with prop drilling - passing props through components that don't actually use them.

What is Prop Drilling?

Prop drilling happens when you pass data through intermediate components:

        
        
          
            
          
          
            
          
        

        function App() {
  const [theme, setTheme] = useState('light');
  return <Layout theme={theme} setTheme={setTheme} />;
}

function Layout({ theme, setTheme }) {
  return <Header theme={theme} setTheme={setTheme} />; // Just passing through
}

function Header({ theme, setTheme }) {
  return <ThemeButton theme={theme} setTheme={setTheme} />; // Still passing
}

function ThemeButton({ theme, setTheme }) {
  return <button onClick={() => setTheme('dark')}>{theme}</button>; // Finally used
}
      

In this example Layout and Header don't use the theme props - they're just data couriers.

When to Use Context

Context is useful when:

  • Data needs to travel through 3+ component levels 
  • Multiple unrelated components need the same data
  • You're passing props that intermediate components don't use

Stick with props when:

  • Simple parent-child relationships (2-3 levels max)
  • Intermediate components actually use the props
  • The data flow is straightforward

Not everything needs Context. Props work fine for most cases.

Building a Theme Toggle: The Four Steps

1. Create the Context

        
        
          
            
          
          
            
          
        

        // src/context/theme.context.jsx

// 1. Import createContext
import { createContext } from "react";

// 2. Create the context
export const ThemeContext = createContext();
      

This establishes the communication channel between components.

2. Create the Provider

On the same theme.context.jsx file we will add the provider.

The provider manages your state and shares it with child components:

        
        
          
            
          
          
            
          
        

        // src/context/theme.context.jsx

// 1. Import createContext and useState
import { useState, createContext } from "react";

// 2. Create the context
export const ThemeContext = createContext();

// 3. Create the provider
export function ThemeProvider({ children }) {

  const [theme, setTheme] = useState('light');

  const toggleTheme = () => {
    setTheme(prev => prev === 'dark' ? 'light' : 'dark');
  }

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  )

}
      

Everything you want to share goes in the value prop.

3. Implement the Provider

Wrap your application with the provider:

        
        
          
            
          
          
            
          
        

        // src/main.jsx

import { ThemeProvider } from './context/theme.context';

root.render(
  <ThemeProvider>
    <App />
  </ThemeProvider>
);
      

Now all components inside <App /> can access the theme context.

4. Consume the Context

Use useContext in any component that needs the data:

        
        
          
            
          
          
            
          
        

        // src/components/SiteNav.jsx

import { useContext } from "react";
import { ThemeContext } from "../context/theme.context";

export default function SiteNav() {

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

  return (
    <nav>
      <a href="#" onClick={toggleTheme}>
        Change Theme ({theme})
      </a>
    </nav>
  )
}
      

No props needed. The component accesses theme data directly.

Why This Works

Your theme state lives in one place. Any component can access it without involving the components in between. The intermediate components stay focused on their own responsibilities.

Context eliminates prop drilling and creates cleaner, more maintainable code when used appropriately.