React Context for Beginners – The Complete Guide (2021)

React Context is a powerful feature that allows you to share state across your app without having to pass props down manually through multiple levels of components. It‘s a great way to manage global data in your app, such as the current user, theme, or language settings.

In this comprehensive guide, we‘ll dive deep into everything you need to know about React Context—what it is, how it works, when to use it, performance considerations, and more. Even if you‘re new to Context, you‘ll come away with a solid understanding and the ability to start using it effectively in your own projects.

Here‘s what we‘ll cover:

Table of Contents

  • What is React Context?
  • The Problem Context Solves: Prop Drilling
  • How to Use React Context
  • Creating and Providing Context
  • Consuming Context with useContext
  • When to Use Context (and When Not To)
  • Context vs Other State Management Solutions
  • Performance Considerations
  • Advanced Context Patterns
  • Alternatives to Context
  • Additional Resources

What is React Context?

React Context provides a way to manage state globally and make it accessible to many components without having to explicitly pass props down through each level of the component tree.

The main idea is that you can create a context that holds some data and then make that context available to any component in the tree that needs it. Components can then consume the context data, and will re-render whenever that data changes.

You can think of context as providing a "central store" for your app‘s data. Rather than each component owning and managing its own local state, context allows that state to be shared and accessed by many components.

The Problem Context Solves: Prop Drilling

To understand what problem context solves, let‘s first look at how data is traditionally passed from component to component in React.

Normally, if a child component needs some data, it would receive that data via props passed down from its parent component. If a deeply nested child needed data from a component higher up in the tree, that data would need to be passed through each intermediate component as a prop—even if those components didn‘t need the data themselves!

This phenomenon is known as "prop drilling". It leads to components that are harder to reuse and maintain, because they require extra props just for the purpose of passing data down the tree.

Here‘s an example of prop drilling:

function App() {
  return (
    <Main theme="light">
      <Header />  
      <Page />
      <Footer />
    </Main>
  );
}

function Main({ theme, children }) {  
  return (
    <div className={theme}>
      {children}  
    </div>
  );
}

function Page() {
  return (
    <div>
      <Sidebar theme="light" />
      <Content theme="light" />  
    </div>  
  );
}

In this example, the theme prop is needed by the Sidebar and Content components, but not by the Main or Page components. Yet we still have to pass it through those components to get it where it needs to go. The Main and Page components are tightly coupled to their children‘s data requirements.

Context solves this by allowing a parent component to provide data to the entire tree below it. Any component can then consume that data, no matter how deep it is, without the data needing to be passed through each intermediate component as a prop.

How to Use React Context

Using context involves three main steps:

  1. Creating the context
  2. Providing the context value
  3. Consuming the context value

Let‘s dive into each of these steps.

Creating and Providing Context

First, you need to create a context using the createContext method from React. This will give you a Context object:

const ThemeContext = React.createContext(‘light‘);

createContext takes an optional default value, which will be used if a component tries to consume the context without a matching Provider above it in the tree.

Next, you need to make this context available to the components that need it. You do this by rendering the Context‘s Provider component and passing the current context value to it via the value prop:

function App() {
  return (
    <ThemeContext.Provider value="dark">
      <Main>
        <Header />
        <Page />  
        <Footer />
      </Main>  
    </ThemeContext.Provider>
  );
}

Now any component under <ThemeContext.Provider> can access the value prop, no matter how deep in the tree it is.

Consuming Context with useContext

To consume the context value in a child component, you first import the context object. Then call the useContext hook, passing it the context:

import ThemeContext from ‘./ThemeContext‘;

function Button() {
  const theme = useContext(ThemeContext);

  return (
    <button className={theme}>
      Click me!  
    </button>
  );
}

useContext returns the current value of the context. The component will automatically re-render whenever the context value changes.

You can also consume context using the render prop pattern with the Consumer component:

<ThemeContext.Consumer>
  {theme => (
    <button className={theme}>
      Click me!
    </button>  
  )}
</ThemeContext.Consumer>

However, useContext is generally preferred as it results in more readable code.

When to Use Context (and When Not To)

So when is it appropriate to use context? Context is best suited for data that is truly "global" and needs to be accessed by many components throughout the tree, such as:

  • User authentication status
  • UI theme (light mode vs dark mode)
  • Current language or locale
  • Access to certain services or data stores

Kent C. Dodds, a prominent React educator and developer, gives this advice:

"The React documentation is very good about the use cases for context. I say [reach for context] when the data is global…But if it‘s not global data, then just stick to props."

However, context is not a good fit for all types of data, and using it unnecessarily can make your components harder to test and reuse. You should avoid putting data in context if:

  • It‘s only needed by a few closely related components
  • It‘s high-frequency data that changes often
  • The data is complex or interrelated

For data that isn‘t truly global, it‘s better to use local component state and props. As the React docs put it:

"If you only want to avoid passing some props through many levels, component composition is often a simpler solution than context."

Context vs Other State Management Solutions

You may be wondering how context compares to other state management solutions like Redux, MobX, or Recoil.

Context is a built-in React feature and requires no additional libraries. It‘s a great choice if you have simple global state needs. Andrew Clark, a core member of the React team, says:

"My personal summary is that new context is ready to be used for low frequency unlikely updates (like locale/theme). It‘s also good to use it in the same way as old context was used. I.e. for static values and then propagate updates through subscriptions. It‘s not ready to be used as a replacement for all Flux-like state propagation."

However, context has some limitations. It‘s not optimized for high-frequency updates, and if not used carefully, can cause unnecessary re-renders.

State libraries like Redux, MobX, and Recoil offer features like richer debugging, middleware support, and state persistence that context alone doesn‘t provide. They may be a better choice for more complex global state.

Redux in particular is known for its "single source of truth"—all global state is kept in a single store. This makes it easier to debug, at the cost of more upfront setup and boilerplate. Dan Abramov, a co-author of Redux, has this to say:

"In a way, React Redux implements the provider pattern to let you access the Redux store from anywhere in the component tree. But it‘s a very specific use of that pattern. Context also has some other capabilities that Redux doesn‘t provide."

Ultimately, the choice of state management solution depends on the specific needs and complexity of your app.

Performance Considerations

One thing to be aware of with context is that whenever the value of a context changes, all components that consume that context will re-render. For data that changes frequently, this can cause performance issues.

Take this example:

function App() {
  const [count, setCount] = useState(0);

  return (
    <CountContext.Provider value={count}>
      <div>
        <Button />
        <button onClick={() => setCount(count + 1)}>
          Increment
        </button>
      </div>
    </CountContext.Provider>
  );
}

Here, the <Button> component will re-render every time count changes—even if the button‘s appearance doesn‘t depend on count.

To avoid unnecessary re-renders, you can:

  • Split your state into multiple contexts so that components only subscribe to the data they need.
  • Memoize context values with useMemo or useCallback.
  • Use the React.memo higher-order component to avoid re-rendering child components.

As a general rule, context is best used for low-frequency state changes. If you need to update state every second or on every keystroke, context might not be the right solution.

Advanced Context Patterns

While the basic usage of context is fairly straightforward, there are some more advanced patterns you can use to get more out of it.

One common pattern is to combine context with a reducer to manage more complex state updates. You can pass a reducer function down as part of the context value:

const TodosDispatch = React.createContext(null);

function TodosApp() {
  const [todos, dispatch] = useReducer(todosReducer, []);

  return (
    <TodosDispatch.Provider value={dispatch}>
      <TodosContext.Provider value={todos}>
        <DeepTree />
      </TodosContext.Provider>    
    </TodosDispatch.Provider>
  );
}

Child components can then consume the dispatch function and use it to dispatch actions that update the todos state.

You can also nest multiple contexts to provide different slices of global state to different parts of the tree:

function App() {
  const [theme, setTheme] = useState(‘light‘);
  const [user, setUser] = useState(null);

  return (
    <ThemeContext.Provider value={theme}>
      <UserContext.Provider value={user}>
        <WelcomePanel />
        <label>
          <input
            type="checkbox"
            checked={theme === ‘dark‘}
            onChange={() => {
              setTheme(t => t === ‘dark‘ ? ‘light‘ : ‘dark‘);
            }}
          />
          Use dark mode
        </label>
      </UserContext.Provider>
    </ThemeContext.Provider>
  );
}

This allows different components to subscribe only to the data they care about.

Alternatives to Context

While context is a great solution for global app state, it‘s not the only way to avoid prop drilling.

For cases where you have groups of related components that need to share state, you might consider component composition instead. You can achieve a similar effect to context by having a parent component manage the shared state and pass it down as props to its children.

Another alternative is to "lift state up" to a common ancestor component and pass it down as props. This works well if you only need to share state between a few components that are closely related in the tree.

If you‘re using context to avoid passing down event handlers as props, another option is to pass down a ref to a child component. The child can then imperatively call methods on the parent‘s ref without needing direct access to the parent‘s state.

Additional Resources

To learn more about React context, check out these additional resources:

You can also find a collection of examples using context in the React docs‘ Context Examples section.

Conclusion

React context is a powerful tool for sharing global data throughout your app without the hassle of manually passing props. It allows you to create a central "store" for your data and easily access that data from any component, no matter how deep in the tree.

In this guide, we covered the fundamentals of working with React context—how to create, provide, and consume context values, when to use context (and when not to), performance considerations, advanced patterns, and more.

While context is not a full-fledged state management system like Redux, it‘s a lightweight built-in solution for sharing state globally. When used judiciously, in combination with local component state, it can help make your app more maintainable and your components more reusable.

Hopefully this guide has given you the knowledge and confidence to start leveraging React context in your own projects!

Similar Posts