Understanding React Context API

May 7th 2020

React’s createContext, useContext, useReducer, Context Provider, Context Consumer and creating custom hooks can all seem confusing. Where does one even start?

The Context API was created to solve a specific problem in react, sharing state down a component tree. Similar to the solution that Redux and React-Redux libraries solve, this solution prevents prop drilling. Prop drilling is when you have to continuously pass props down through a component tree in order for a component “way down” in the application to use that prop. If you’ve used react for sometime you have probably experienced this pain point.

Prop Drilling

Usually you manage state at a high level component such as the <App /> component, and if that root component needs to access a state value, as well as a “grand-child” component that needs to access that value, then the state must be passed down as a prop all the way down. For example, say in your root component you need to be aware of a property in your state called theme. That theme property will determine the background color for your app, if it’s value is light then the background color of the app will be white, but if the value is dark the background color will be black. Furthermore, let’s say we have a List component in our App and that list component has a ListItem component and that ListItem component has a Button component. The Button component needs to be aware of the theme property in the root state as well because it’s color will change depending on the value of the theme. In a traditional architecture we would have to pass the value down as a prop such as:

import React, { useState } from "react"

const App = (props) => {
  const [theme, setTheme] = useState('light')
  const bg = theme === 'light' ? 'white' : 'black'

  return (
     <div style={{
       background: bg
     }}>
       <List theme={theme} />
     </div>
  )
}

const List = ({ theme }) => (
  <ul>
    <ListItem theme={theme} text="light" />
    <ListItem theme={theme} text="dark" />
  </ul>
)

const ListItem = ({ theme, text }) => (
  <li>
    <Button theme={theme}>
      {text}
    </Button>
  </li>
)

const Button = ({ children, theme }) => {
  const bgColor = theme === 'light' ? 'darkgray' : 'lightgray'
  return (
    <button
      style={{
        backgroundColor: bgColor
      }}>
      { children }
    </button>
  )
}

export default App

As you can see we had to pass the theme prop down from App to List to ListItem and finally to Button. On top of that, List and ListItem did not have to (or care to) know about the value of theme in our state and is irrelevant to them. Wouldn’t it be nice if only App and the Button component would have to know about theme value and we wouldn’t have to prop drill it all the way down? Welcome in the Context API!

Welcome to the Context API

Context provides a way to pass data through the component tree without having to pass props down manually at every level.

https://reactjs.org/docs/context.html

Let’s look at how we would refactor our small application using the Context API and also we will use the reducer architecture with useReducer hook from React to enhance our example so it could be extendable to other global state properties besides our theme property.

First, we’ll create a new folder in our project directory called hooks and inside that folder we can create a file called useTheme.js. The useTheme.js file will essentially become our state management for our application as we will abstract our application state from our App component. First we’ll have to import some helpers from react for us to use the Context API and also the reducer architecture:

import React, { createContext, useContext, useReducer } from 'react'

We will use the createContext method to create our context object. A context object that is created by createContext gives us access to a Provider property that functions as a Higher Order Component in which we will wrap our component tree in that needs access to our state. Remember, our component tree is just the components that make up our App including: App, List, ListItem and Button. Any component “down stream” in our component tree can become a consumer to access our context value. Let’s look at creating a context in our useTheme.js file:

import React, { createContext, useContext, useReducer } from 'react'

const ThemeContext = createContext()

Creating a Reducer to Manage State

Next we will create a reducer which will help us set and manage our state. We can create a variable that holds our default state such as:

const DEFAULT_STATE = {
  theme: "light"
}

Not complicated but this will demonstrate what our state object will look like. Now we can create our reducer. As convention, for reducers our reducer function will accept two arguments: state object and an action object. For our actions object we will have an action.type which is the action we want to perform on our state and action.payload which will be the value of the state property we are setting. Let’s look at our reducer:

const reducer = (state, action) => {
  switch(action.type){
    case 'theme':
      return { ...state, theme: action.payload }
    default:
      return DEFAULT_STATE
  }
}

This is a very simple reducer for now but it demonstrates the fundamentals of using a reducer and checking the action.type to change (and return) our state. Putting this together so far our useTheme.js file should look like:

import React, { createContext, useContext, useReducer } from 'react'

const ThemeContext = createContext()

const DEFAULT_STATE = {
  theme: "light"
}

const reducer = (state, action) => {
  switch(action.type){
    case 'theme':
      return { ...state, theme: action.payload }
    default:
      return DEFAULT_STATE
  }
}

Defining a Context Provider

Now that we created our Context as ThemeContext and we have defined our default state and created a reducer to handle the changes in state we can focus on our provider. Since we’ll be exporting our provider to use to wrap our <App /> component we’ll need to export it as a variable:

const ThemeProvider = ({ children }) => {
  <ThemeContext.Provider value="light">
    { children }
  </ThemeContext>
}

Context Providers accepts a value prop to be passed to consuming components that are descendants (in our component tree) of this Provider. For now we hard coded this value so now that any child component that needed access to this value could use:

import React, { useContext } from 'react'
import { ThemeContext } from '../hooks/useTheme'

//assuming this component is a child of our <ThemeProvider>
const ChildComponent = (props) => {
  const theme = useContext(ThemeContext)
  console.log(theme) // would console log "light"
  return (
    <span>The theme is: {theme}</span>
  )
}

But since we are using a reducer example we can change the value prop in our ThemeProvider function to use our reducer:

const ThemeProvider = ({ children }) => {
  <ThemeContext.Provider value={ useReducer(reducer, DEFAULT_STATE) }>
    { children }
  </ThemeContext>
}

The useReducer hook is similar to useState in that it returns an array with the first value the state that is returned from our reducer function and the second value of the array is a dispatcher function which we can use to dispatch actions and in turn change our state. Since we’re using a reducer as the Provider value we can define our useTheme.js file further by creating a custom hook and exporting that hook which can be imported into any of our consumer components to get our state context.

Creating a Custom Hook to for Consuming Components

Let’s continue our useTheme.js and define our custom hook:

const useTheme = () => {
  // short version:
  // return useContext(ThemeContext)
  // long version but more declarative
  const [state, dispatcher] = useContext(ThemeContext)
  return [state, dispatcher]
}

Let’s wrap up our useTheme.js file by exporting our ThemeProvider and useTheme hook:

import React, { createContext, useContext, useReducer } from 'react'

const ThemeContext = createContext()

const DEFAULT_STATE = {
  theme: "light"
}

const reducer = (state, action) => {
  switch(action.type){
    case 'theme':
      return { ...state, theme: action.payload }
    default:
      return DEFAULT_STATE
  }
}

const ThemeProvider = ({ children }) => {
  <ThemeContext.Provider value={ useReducer(reducer, DEFAULT_STATE) }>
    { children }
  </ThemeContext>
}

const useTheme = () => {
  const [state, dispatcher] = useContext(ThemeContext)
  return [state, dispatcher]
}

export { ThemeProvider, useTheme }

Now with that wrapped up we can refactor our App so we can utilize our new hook. First, we need to wrap our <App /> in our <ThemeProvider> higher order component. In our index.js file with defines ReactDOM.render we can import our ThemeProvider and wrap our <App />. Let’s change index.js so that:

import React from "react";
import ReactDOM from "react-dom";
import { ThemeProvider } from "./hooks/useTheme";
import App from "./components/App";

ReactDOM.render(
  <ThemeProvider>
    <App />
  </ThemeProvider>,
  document.getElementById("root")
);

With our <App /> component wrapped in our <ThemeProvider> we are able to use our custom hook useTheme to consume the value we defined in our ThemeProvider function which is the returned value of useReducer which would be [state, dispatcher]

Returning to our App.js file we can refactor our App to make use of our custom hook and access a “global” state without prop drilling our state down. Let’s look at our App.js file:

import React from "react";
import { useTheme } from "../hooks/useTheme";

const App = props => {
  const [state] = useTheme();
  const bg = state.theme === "light" ? "#ffffff" : "#000000";

  return (
    <div
      style={{
        background: bg
      }}
    >
      <List />
    </div>
  );
};

const List = () => (
  <ul>
    <ListItem value="light" />
    <ListItem value="dark" />
  </ul>
);

const ListItem = props => (
  <li>
    <Button {...props} />
  </li>
);

const Button = ({ value }) => {
  const [state, dispatcher] = useTheme();
  const bgColor = state.theme === "light" ? "#333333" : "#eeeeee";
  const textColor = state.theme === "light" ? "#ffffff" : "#000000";

  return (
    <button
      style={{
        backgroundColor: bgColor,
        color: textColor
      }}
      onClick={() => {
        dispatcher({ type: "theme", payload: value });
      }}
    >
      {value}
    </button>
  );
};

export default App;

The two important changes to highlight is we are using our useTheme hook to get the state in our parent App function and conditionally set the background color of our app. Secondly, we were able to remove the theme prop from our component tree and the Button component was able to access our state via the useTheme custom hook that we created. We also made some more changes to our Button component in order to dispatch an action which updates our state. Let’s review that portion quickly:

const Button = ({ value }) => {
  const [state, dispatcher] = useTheme();
  const bgColor = state.theme === "light" ? "#333333" : "#eeeeee";
  const textColor = state.theme === "light" ? "#ffffff" : "#000000";

  return (
    <button
      style={{
        backgroundColor: bgColor,
        color: textColor
      }}
      onClick={() => {
        dispatcher({ type: "theme", payload: value });
      }}
    >
      {value}
    </button>
  );
};

First, we are accepting a prop called value that is inherited from our List component and passed down through our ListItem component. This value represents a theme option so when a user clicks on a button it will change the theme to the respective value. Second, we are using our custom hook useTheme to get the state of our application (which is returned by our reducer) and also we’re destructuring the array to get the dispatcher function which sends an action to our reducer and changes our state. We are setting an onClick callback handler function on our button and invoking our dispatcher function inside our click handler, passing in the type and the payload which is the value of the button the user clicked on.

If you want to check out the full results of the application you can use the CodeSandbox link: https://codesandbox.io/s/j1ebq

In this tutorial we looked at how to use the Context API in React to avoid prop drilling and use a “global” state in our component tree. We also were able to utilize a useReducer hook with a custom reducer we created in order for us to help manage our state. Finally, we created a custom hook that we could reuse in our consuming components that would get the current value of our state, and also provide a dispatcher function that we could use to update our state. Thanks for taking the time to explore React’s Context API with me and hopefully this simple example helped you understand the power of what can be accomplished with the API. Until next time, stay curious, stay creative.