Skip to Content
Lesson 5

Context API

Workplace Context

In larger React applications, passing data through many layers of components via props (a technique known as “prop drilling”) can become cumbersome and make components less reusable. The Context API provides a way to share values like user authentication status, UI theme, or application configuration between components without explicitly passing a prop through every level of the tree. Understanding Context is key to managing global or widely shared state efficiently and cleanly in professional React projects.

Learning Objectives

By the end of this lesson, learners will be able to:

  • Explain what the Context API is and the problem it solves (prop drilling).
  • Create a new context using React.createContext.
  • Provide context to a tree of components using a Context Provider.
  • Consume context values in function components using the useContext Hook.
  • Consume context values using a Context Consumer component (for awareness).
  • Determine appropriate use cases for Context API versus local state or other state management solutions.
  • Understand basic performance considerations when using Context.

The Problem with Prop Drilling

Before diving into Context, it’s important to understand the problem it primarily solves: prop drilling.

Prop drilling refers to the situation where you need to pass data from a component high up in the component tree to a component deep down in the tree. To do this without Context, you have to pass the prop through all intermediate components, even if those components don’t need the data themselves.

Example of Prop Drilling

App.js
// (has the data) function App() { const theme = "dark"; return <Toolbar theme={theme} />; }
Toolbar.js
// (doesn't need theme, just passes it down) function Toolbar({ theme }) { return <ThemedButton theme={theme} />; }
ThemedButton.js
// (actually uses the theme) function ThemedButton({ theme }) { return <button className={theme}>I am a themed button</button>; }

In this example, Toolbar doesn’t use theme but has to accept it as a prop and pass it down to ThemedButton. If there were more intermediate components, theme would have to be “drilled” through all of them. This can make components less flexible and more difficult to refactor.


What is Context API?

The React Context API provides a way to pass data through the component tree without having to pass props down manually at every level. It’s designed to share data that can be considered “global” for a tree of React components, such as the current authenticated user, theme, or preferred language.

Context works in three main parts:

  1. Creating Context: You create a Context object using React.createContext(). This object will hold the data you want to share.
  2. Providing Context: You use a Context.Provider component to wrap a part of your component tree. This Provider accepts a value prop, which is the data you want to make available to all components underneath it.
  3. Consuming Context: Components that need access to the context data can subscribe to it. There are two ways to consume context:
    • The useContext Hook (most common and preferred in function components).
    • A Context.Consumer component (more verbose, used in class components or older function component patterns).

Creating and Using Context

Let’s walk through the steps of using Context.

1. Creating Context (React.createContext)

You create a Context object using React.createContext(defaultValue). The defaultValue argument is only used when a component does not have a matching Provider above it in the tree. This can be useful for testing components in isolation without wrapping them.

theme-context.js
import React from 'react'; // Create a context with a default value (e.g., 'light' theme) const ThemeContext = React.createContext('light'); export default ThemeContext;

It’s common to create context in a separate file (e.g., theme-context.js, user-context.js) to make it easily importable by any component that needs it.

2. Context Provider (<MyContext.Provider>)

Every Context object comes with a Provider React component that allows consuming components to subscribe to context changes. The Provider component accepts a value prop to be passed to consuming components that are descendants of this Provider. One Provider can be connected to many consumers. Providers can be nested to override values deeper within the tree.

App.js
import React, { useState } from 'react'; import ThemeContext from './theme-context'; import Toolbar from './Toolbar'; // A component that will eventually consume the context function App() { const [theme, setTheme] = useState('dark'); // Manage the theme state const toggleTheme = () => { setTheme(prevTheme => (prevTheme === 'light' ? 'dark' : 'light')); }; return ( <ThemeContext.Provider value={{ theme, toggleTheme }}> <button onClick={toggleTheme}>Toggle Theme</button> <Toolbar /> </ThemeContext.Provider> ); } export default App;

In this example, the App component provides the theme state (which is an object { theme, toggleTheme }) to all components within the ThemeContext.Provider.

Important: All consumers that are descendants of a Provider will re-render whenever the Provider’s value prop changes. The propagation of context changes is not subject to the shouldComponentUpdate method in class components or React.memo in function components. Careful consideration of the value prop is needed for performance (see Performance Considerations section).

3. Consuming Context

The useContext Hook

The useContext Hook is the modern and preferred way to consume context in function components. It accepts a context object (the value returned from React.createContext) and returns the current context value for that context. The current context value is determined by the value prop of the nearest <MyContext.Provider> above the calling component in the tree.

ThemedButton.js
import React, { useContext } from 'react'; import ThemeContext from './theme-context'; function ThemedButton() { const { theme, toggleTheme } = useContext(ThemeContext); // Consume the context const buttonStyle = { background: theme === 'dark' ? '#333' : '#FFF', color: theme === 'dark' ? '#FFF' : '#333', padding: '10px', border: `1px solid ${theme === 'dark' ? '#FFF' : '#333'}` }; return ( <button style={buttonStyle} onClick={toggleTheme}> Themed Button (Current: {theme}) </button> ); }
Toolbar.js
// (no longer needs to know about theme explicitly) function Toolbar() { return ( <div> <ThemedButton /> </div> ); } export default Toolbar;

When the nearest <MyContext.Provider> above the component updates, the useContext Hook will trigger a re-render with the latest context value.

The Context.Consumer Component (for awareness)

Before Hooks, the Context.Consumer component was the primary way to consume context. It requires a function as a child (a render prop). The function receives the current context value and returns a React node.

ThemedButton.js
import React from 'react'; import ThemeContext from './theme-context'; function ThemedButton() { return ( <ThemeContext.Consumer> {({ theme, toggleTheme }) => { const buttonStyle = { background: theme === 'dark' ? '#333' : '#FFF', color: theme === 'dark' ? '#FFF' : '#333', // ... other styles }; return ( <button style={buttonStyle} onClick={toggleTheme}> Themed Button (Current: {theme}) </button> ); }} </ThemeContext.Consumer> ); }

While you will primarily use useContext in modern React, it’s good to recognize Context.Consumer if you encounter it in older codebases or class components.


When to Use Context

Context is primarily used when some data needs to be accessible by many components at different nesting levels. Apply it sparingly because it makes component reuse more difficult.

Good use cases for Context:

  • Theming: Passing down theme information (colors, fonts) to many components.
  • User Authentication: Sharing the current authenticated user object and authentication status.
  • Application Configuration: Providing application-level settings or configuration.
  • Language/Localization: Sharing the current language preference for internationalization.

When not to use Context (or use with caution):

  • Replacing all prop passing: If only a few components need the data, or if it’s only being passed down one or two levels, prop drilling might still be simpler and more explicit.
  • High-frequency updates: If the context value changes very frequently, it can cause performance issues as all consuming components will re-render. For very dynamic state, consider local state, reducers, or more optimized state management libraries.
  • State that is not truly global: If the data is only relevant to a small subsection of your component tree, consider component composition or local state within that subsection.

Before using Context, ask yourself: “Does this data need to be accessed by many components at different levels, and would prop drilling be overly cumbersome?”


Performance Considerations

When a Provider’s value prop changes, all components consuming that context will re-render, even if they don’t directly use the part of the value that changed. This can lead to performance issues if not managed carefully.

  1. Object Identity for value Prop: If you pass an object as the value to a Provider, be careful about creating a new object on every render of the Provider’s parent component.

    // Potentially problematic: function App() { const [user, setUser] = useState({ name: 'Alice' }); const [theme, setTheme] = useState('dark'); // This object is new on every App render, causing all consumers to re-render return <UserContext.Provider value={{ user, theme }}>...</UserContext.Provider>; }

    Even if user and theme haven’t changed, a new { user, theme } object is created, causing consumers to re-render. To fix this, memoize the value or split contexts.

  2. Splitting Contexts: If you have multiple pieces of global data that change at different rates, consider splitting them into separate Contexts. For example, ThemeContext (changes rarely) and CurrentUserContext (might change on login/logout). This way, components only subscribe to the context they need, and only re-render when that specific piece of data changes.

  3. Memoizing the Provider Value: You can use useMemo to memoize the value object passed to the Provider, ensuring it only changes when its underlying data actually changes.

    // Better: function App() { const [user, setUser] = useState({ name: 'Alice' }); const [theme, setTheme] = useState('dark'); const contextValue = useMemo(() => ({ user, theme }), [user, theme]); return <AppContext.Provider value={contextValue}>...</AppContext.Provider>; }
  4. React.memo on Consumers: While React.memo on a consuming component won’t prevent re-renders caused by context value changes by default, you can use it in conjunction with careful context splitting and selective consumption if a child component is expensive to render and doesn’t use all parts of a large context value.

Understanding these nuances is important for using Context effectively without inadvertently degrading your application’s performance.


Activity 1: Implement a Theme Context

Task: Create a simple application that uses Context to allow users to toggle between a light and dark theme.

Requirements:

  1. Create a ThemeContext that will provide the current theme ('light' or 'dark') and a function to toggle the theme.
  2. Create a top-level App component that acts as the ThemeProvider and manages the theme state.
  3. Create a Navbar component that consumes the theme context to adjust its styling.
  4. Create a Content component that also consumes the theme context for its styling.
  5. Include a button (perhaps in Navbar or App) to toggle the theme.

Completed Example:

(Take a look at the bottom left of this page! We use a global theme state within this application.)


Activity 2: Create a User Context

Task: Implement a UserContext to manage and provide user authentication status and information.

Requirements:

  1. Create a UserContext that will provide currentUser (an object like { name: string } or null) and functions login(userName) and logout().
  2. The App component should be the UserProvider and manage the currentUser state.
  3. Create a UserProfile component that displays the current user’s name if logged in, or a “Please log in” message if not.
  4. Create a LoginButton component that, if no user is logged in, shows a button to log in (e.g., with a predefined name).
  5. Create a LogoutButton component that, if a user is logged in, shows a button to log out.

Completed Example:

(Canvas, and many of your other most-used websites use some form of global context to manage user authentication.)


Knowledge Check

What is the primary problem that the React Context API aims to solve?

  • Select an answer to view feedback.

How do you provide a context value to a tree of components in React?

  • Select an answer to view feedback.

What is the most common and preferred way to consume a context value within a React function component?

  • Select an answer to view feedback.

Which of the following is generally considered a good use case for the React Context API?

  • Select an answer to view feedback.

What is a potential performance issue to be mindful of when using the Context API?

  • Select an answer to view feedback.

Summary

The Context API is a powerful tool in React for managing state that needs to be accessed by many components at different nesting levels, effectively solving the problem of prop drilling. By creating a context, providing a value with a Provider, and consuming it with the useContext Hook, you can make global data accessible throughout a component tree. While convenient, it’s important to use Context judiciously and be mindful of performance implications, especially regarding the stability of the value prop and the potential for unnecessary re-renders.


References

Additional Resources