$ emrebener
home topics react react’s context api demystified: a deep dive into usecontext

React’s Context API Demystified: A Deep Dive into useContext

author: emre bener read time: 6 min about: usecontext, react context api, react
published: updated: mentions: createcontext, prop drilling, state management, redux, mobx

Sharing state across a React tree gets awkward once the components in the middle stop caring about the data they’re forwarding. The Context API, paired with the useContext hook, lets components grab shared state without props threading through every level. This post walks through how useContext works, how the Context API fits around it, and where it stops being the right tool.

1. useContext Definition

The useContext hook in React is a fundamental hook used for state management, enabling components to access shared values from a context without the need to pass props through every level of the component tree (also known as “prop drilling”). With Context, you can define a “global” value at one level in your component tree and make it accessible to any component within that tree, regardless of how deeply nested it is.

==Eliminating prop drilling== keeps data flow legible and the tree easier to refactor. Common payloads: themes, the current language, auth state, anything a lot of components need but most of them shouldn’t have to forward.

💡 useContext call must be placed outside of any components.

2. useContext Reference

const value = useContext(SomeContext)
  • SomeContext: This is the context object you want to access, typically created using createContext API. It contains a Provider component and a ==Consumer==. The “Consumer” component is only relevant for class components.
  • useContext(SomeContex**: This hook takes the SomeContext object as its argument and returns the current value of that context. The value returned is the one provided by the nearest SomeContext.Provider component that wraps the component where useContext is called.
  • const value: This is the variable that stores the value returned by useContext. This value could be anything that the context provider supplies, such as an object, a string, a number, or a function. If there is no such provider, then the returned value will be the defaultValue you have passed to createContext for that context. The returned value is always up-to-date. Moreover, React automatically re-renders components that read the context if it changes.

3. Context API (createContext) Reference

const context = createContext(defaultValue)

Parameters

  • defaultValue: The value that you want the context to have when there is no matching context provider in the tree above the component that reads context. If you don’t have any meaningful default value, specify null. The default value is meant as a “last resort” fallback.

Returns

When you create a context using createContext, it returns two components: a Provider and a Consumer.

  • The Provider component is used to “provide” the context value to all of its children components, making the shared data available to them. The Provider component has a single prop called “value” which holds the context value. This value can be any type of data—such as a string, object, or function—and it can be dynamic, updating over time as the state changes.
  • The Consumer component, on the other hand, is used in class components to access context value. When using the Consumer, you wrap the class component (or part of the component’s JSX) with the Consumer component, and inside it, you provide a function as a child. This function receives the current context value as an argument and returns the JSX that should be rendered.

Note that function components don’t use the Consumer component to access context data. Instead, they use the useContext hook. The Provider component, however, is used by both function components and class components.

4. Example

To create a context, use createContext outside of any components. Here is how you would typically create a context:

// ThemeContext.js  
  
import { createContext } from 'react'  
const ThemeContext = createContext('light')

You would then import this context when you are using the Provider component, the Consumer component or the useContext hook. Let’s have a look at each case.

Provider Component

The Provider component serves the context value to all of its children. Below, we build a ThemeProvider that pairs a state variable with the context it exposes, a pattern you’ll see a lot.

// ThemeProvider.js  
  
import { useState } from 'react'  
import ThemeContext from './ThemeContext'  
  
export default function ThemeProvider({ children }) {  
  const [theme, setTheme] = useState('light')  
    
  const toggleTheme = () => {  
    setTheme(theme === 'light' ? 'dark' : 'light')  
  };  
  return (  
    <ThemeContext.Provider value={{ theme, toggleTheme }}>  
      {children}  
    </ThemeContext.Provider>  
  )  
}

A toggleTheme function rides along inside the value, so any child can flip the theme without owning the state. The context value here is an object, and whatever you put into it becomes a property on the value consumers read back — in this case, theme and toggleTheme.

Consumer Component

With the Provider component down, what’s left is to consume the context value in the children components. Keep in mind that the Consumer component is only relevant for class components, and function components instead use the useContext hook, which we’ll have a look at in the next section.

Here is how you would use the Consumer component in a class component that’s the children of a context Provider:

// ThemeButtonClass.js  
  
import { Component } from 'react'  
import ThemeContext from './ThemeContext'  
export default class ThemeButtonClass extends Component {  
  render() {  
    return (  
      <ThemeContext.Consumer>  
        {({ theme, toggleTheme }) => (  
          <button   
            onClick={toggleTheme}   
            style={{ background: theme === 'light' ? '#fff' : '#333', color: theme === 'light' ? '#000' : '#fff' }}  
          >  
            Toggle Theme  
          </button>  
        )}  
      </ThemeContext.Consumer>  
    )  
  }  
}

Then, we would use the ThemeProvider component to wrap this component like so:

// App.js  
  
import ThemeProvider from './ThemeProvider'  
import ThemeButtonClass from './ThemeButtonClass'  
export default function App() {  
  return (  
    <ThemeProvider>  
      <div>  
        <h1>Click to toggle theme:</h1>  
        <ThemeButtonClass />  
      </div>  
    </ThemeProvider>  
  )  
}

Thus, since ThemeButtonClass component is a children of ThemeProvider component, it can access ThemeContext. In this example, since ThemeButtonClass is a class component, we used the Consumer component in it by importing ThemeContext, and accessed the context value with it.

The function inside the Consumer component can be a bit confusing at first, so let’s break it down step by step.

The piece worth slowing down on:

// ThemeButtonClass.js  
  
// ...  
<ThemeContext.Consumer>  
  {({ theme, toggleTheme }) => (  
    <button   
      onClick={toggleTheme}   
      style={{ background: theme === 'light' ? '#fff' : '#333', color: theme === 'light' ? '#000' : '#fff' }}  
    >  
      Toggle Theme  
    </button>  
  )}  
</ThemeContext.Consumer>  
// ...

Here, the ThemeContext.Consumer component acts as a wrapper, and listens to the ThemeContext to provide its values into the contained function (from the nearest Provider component above it in the component tree).

The function inside the Consumer isn’t a regular function you call from your own code. It’s a render prop: React calls it for you and passes the current context value in as the argument.

Since our context value has two properties, there is also an object destructuring happening where we did {({ theme, toggleTheme }) => ...}. This is done to pull out theme and toggleTheme from the context value object directly.

Finally, with all of the needed context data, we used theme to determine the style, and we passed the toggleTheme function to the button’s onclick event.

Accessing a context from a class component is significantly more complex compared to function components. In the next section, we’ll have a look at how function components access context with the useContext hook.

useContext Hook

Lets create a component similar to ThemeButtonClass, except make it a function component instead of a class component. To access a context from a function component, we will use the useContext hook instead of a Consumer component.

// ThemeButtonFunction.js  
  
import { useContext } from 'react'  
import ThemeContext from './ThemeContext'  
export default function ThemeButton() {  
  const { theme, toggleTheme } = useContext(ThemeContext); // object destructuring  
  return (  
    <button   
      onClick={toggleTheme}   
      style={{ background: theme === 'light' ? '#fff' : '#333', color: theme === 'light' ? '#000' : '#fff' }}  
    >  
      Toggle Theme  
    </button>  
  )  
}

Then, assume the following component hierarchy:

// App.js  
  
import ThemeProvider from './ThemeProvider'  
import ThemeButtonFunction from './ThemeButtonFunction'  
export default function App() {  
  return (  
    <ThemeProvider>  
      <div>  
        <h1>Click to toggle theme:</h1>  
        <ThemeButtonFunction />  
      </div>  
    </ThemeProvider>  
  )  
}

useContext collapses the whole Consumer-wrapper dance into one line. We call it, destructure theme and toggleTheme off the returned value and use them where we need them.

5. Benefits & Limitations of Context API

Benefits of Context API

  • No more prop drilling: deeply nested components read the value directly, and the components in between stop carrying props they don’t use.
  • Cleaner data flow: the wiring lives in one place, so renaming or reshaping the shared value is a one-file change instead of a tree-wide one.
  • Flexible payloads: anything goes inside a context value — themes auth state, feature flags, even callbacks for global events.

Limitations of the Context API

Context handles simple sharing well. Once the state grows or churns, the cracks show:

  • Re-renders cascade: every consumer re-renders when the context value changes, even the ones that only read one field off a fat value object. In large trees that bill adds up.
  • Complex State Management: For more complex state management scenarios, such as handling deeply nested or highly dynamic state, you might want to consider using more advanced tools like Redux or MobX.