sajad torkamani

In a nutshell

Many React applications manage global-like data (e.g., locale preference, user authentication status, UI theme, etc.) that is used by several components at different layers of the component tree. Context lets you pass such data through the component tree without having to pass props down manually at every level of the tree.

Prop-drilling: the problem that context solves

Suppose you had an app with the following components (CodeSandbox):

const App = () => <Navbar user={{ name: 'John Doe' }} />

const Navbar = ({ user }) => (
  <nav>
    <UserDetails user={user} />
  </nav>
)

const UserDetails = ({ user }) => <div>User: {user.name}</div>

Because UserDetails requires the user prop, you must pass down the user prop through the intermediate Navbar component. In this example, there’s only one intermediate component (Navbar), but in many React apps, the component tree will have many layers so you must thread props through several layers.

This is a common problem and is often referred to as “Prop Drilling“. React Context exists to solve this exact problem.

How Context solves prop-drilling

Instead of passing down the user prop through every intermediate component between App and UserDetails, you can create and use a UserContext like this (CodeSandbox):

// Create a context for the current user (with a default value).
export const UserContext = React.createContext(null)

const App = () => (
  // Use a Provider to pass the current user to the tree below.
  // Any component can read it, no matter how deep it is.
  // In this example, we're passing {user: "John Doe"} as the 
  // current value.
  <UserContext.Provider value={{ name: 'John Doe' }}>
    <Navbar />
  </UserContext.Provider>
)

// Navbar doesn't have to pass the user down explicitly anymore.
const Navbar = () => (
  <nav>
    <UserDetails />
  </nav>
)

const UserDetails = ({ user }) => {
  const userContext = useContext(UserContext)

  return <div>User: {userContext.name}</div>
}

Again, this example has only one intermediate component (Navbar), but in most React apps, you will have many more intermediate components. The Context API helps you easily share global-like data with several components regardless of where they are in the component tree. All you have to do is:

How context updates trigger re-renders

When the nearest <MyContext.Provider> above the consuming component updates, the component will re-render with the latest value prop of the nearest <MyContext.Provider>.

Even if an ancestor component uses React.memo or shouldComponentUpdate, the component consuming the context will still re-render.

Subscribe to context in a class component

Once you’ve created a context using React.createContext, you can assign that context to the contextType class property on your component. You can then use this.context to consume the nearest current value of the context.

class MyClass extends React.Component {
  componentDidMount() {
    let value = this.context;
    /* perform a side-effect at mount using the value of MyContext */
  }
  componentDidUpdate() {
    let value = this.context;
    /* ... */
  }
  componentWillUnmount() {
    let value = this.context;
    /* ... */
  }
  render() {
    let value = this.context;
    /* render something based on the value of MyContext */
  }
}

MyClass.contextType = MyContext;

You can also use this alternative syntax:

class MyClass extends React.Component {
  static contextType = MyContext;
  render() {
    let value = this.context;
    /* render something based on the value */
  }
}

How to implement a dynamic context

What if you need to be able to update a context? For example, suppose your app supports a light theme by default but lets users switch to a dark theme.

To implement dynamic context, you’d do something like the following (CodeSandbox):

1. Create context that uses state internally

import React, { useState } from 'react'
import Navbar from './Navbar'

const DEFAULT_THEME = 'dark'

// Create a context for the current user (with a default value).
export const ThemeContext = React.createContext({
  theme: DEFAULT_THEME,
  toggleTheme: () => {}
})

const App = () => {
  const [theme, setTheme] = useState(DEFAULT_THEME)

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

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

export default App

2. Call the context method that updates the context’s internal state

import { useContext } from 'react'
import { ThemeContext } from './App'

// Navbar doesn't have to pass the user down explicitly anymore.
const Navbar = () => {
  const { theme, toggleTheme } = useContext(ThemeContext)

  return (
    <nav>
      <p>
        Theme: <strong>{theme}</strong>
      </p>
      <button onClick={() => toggleTheme()}>Toggle theme</button>
    </nav>
  )
}

export default Navbar

Create custom hook to easily access context

Instead of:

const { theme, toggleTheme } = useContext(ThemeContext)

You can encapsulate a context behind a custom hook:

const { theme, toggleTheme } = useTheme()

useTheme can be defined as something like this:

export function useTheme() {
  const context = useContext(ThemeContext)

  if (context === null) {
    throw new Error(
      'useTheme requires components to be wrapped in a ThemeProvider'
    )
  }

  return context
}

Other notes

  • Here’s a Sandbox demonstrating how to write a simple AppSettings context.

Sources

Tagged: React