Now that React context has become more established in the community we are seeing a lot of great usages of it. Reflecting on a previous post about Higher-order components (HOC) vs Render props, I rarely use HOC and now generally choose between Context or Render props. With the introduction of hooks and in particular useContext hook, React context is more accessible and has become a go-to approach to solving complex state management. However, there are other options to handle these cross-cutting concerns and so we should be clear on why we are using context. Let’s explore why and how to use React context.

Side view of steps in all white Photo by Stéphane Mingot on Unsplash

Why React context

The React docs have an in-depth post covering all aspects of React context, but here I will provide a summary of it and how to use it.

What is the purpose of React context?

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

For example, you might have a container component responsible for fetching user data that is consumed by several components further down the component tree. If you were building a profile page to show an avatar, there might be a navbar containing a dropdown which has an image element for the avatar. Further down the page, there is a profile wrapper and inside that a header component to render the avatar. The intermediate components don’t use the data, NavBar, Dropdown and ProfileWrapper, only pass the data along.

Passing data via props is a poor approach because:

  • Exposing data to components that don’t need to know about it
  • It’s repetitive code and makes the component props more verbose
  • Difficult and frustrating developer experience to follow the flow of data, especially in a large and complex component tree

React context removes these issues by:

  • Explicitly declaring the use of context and therefore handling which components have access to that data
  • Less repetitive and not polluting the component props
  • Using context makes it clear where the data is defined

Using context to manage data flow in a complex component tree can help make code easier to rationalise.

Areas you might use React context

  • Site settings/preferences for a user
  • Styling themes, eg, making it easy to change switch between light/dark theme
  • User details, hide/show user views depending on if they are logged in
  • E-commerce basket management, showing items added to a basket when searching and adding items

The above examples could be regarded as global to an application. However, it does not mean context should be used only for global features. It can be used further down the component tree to manage the state of a complex feature. For example, a multi-step form where next questions are dependent on previous answers.

More options

It does sound like React context can be used for anything, however there are other options for cross-cutting concerns which may be preferred. Context would be excessive for handling local state with a component tree of two or three levels. With the principle of keeping code easy to reason about here are other approaches:

  • To handle state for a basic form you can use the Render props approach, like the popular form library Formik.
  • When two components share the same state you can use lifting state up as an option.

How to use it?

I wrote a post in November covering manage complex state with React useReducer and useContext. This is where I first mention Context and it comes with a todo app coding example on CodeSandbox. I’ve decided to fork that sandbox and refactor it to have a better todo API in React context.

Let’s begin

React context is made of two parts a Provider and a Consumer. The provider is where the values are defined that the consumer can access. Following the good practice of encapsulation we won’t expose the actual React provider but a wrapping component which will define the values. This simplifies the usage of ToDosContext and exposes essential parts of the API to be used. Below is a code example which exhibits this:

// todo-context.js
const TodosContext = React.createContext();

export function TodoProvider({ children }) {
  const [todoList, dispatch] = useReducer(toDoReducer, initialState);
  return (
    <TodosContext.Provider value={ { todoList, dispatch } }>
      {children}
    </TodosContext.Provider>
  );
}

Following the same method of abstraction let’s not expose the dispatch function but instead provide the methods to manage the tasks, add, remove and mark:

// todo-context.js
// ...
export function TodoProvider({ children }) {
  const [todoList, dispatch] = useReducer(toDoReducer, initialState);
  const actions = {
    add(text) {
      dispatch(addAction(text));
    },
    mark(taskId, done) {
      dispatch(markAction(taskId, done));
    },
    remove(taskId) {
      dispatch(deleteAction(taskId));
    }
  };
  return (
    <TodosContext.Provider value={ { todoList, actions } }>
      {children}
    </TodosContext.Provider>
  );
}

From todo-context.js we can also export the consumer like so:

export const TodosConsumer = TodosContext.Consumer;

Also export the main TodosContext on default:

export default TodosContext;

Let’s look at how to access and use the add action in the todo context. One option is to use the React hook useContext. This hook takes the ToDosContext and can access the actions, which are then destructed out. The add action is called in the handleSubmit function to add a new task. See snippet below:

// input-task.js
// InputTask ...
const [task, setTask] = useState("");
const {
  actions: { add }
} = useContext(TodosContext);

function handleSubmit(e) {
  e.preventDefault();
  add(task);
  setTask("");
}
// ...

Another way to access the context is to use the consumer component. In the TaskList component below the TodosConsumer reads the todoList array. This uses the Render props pattern to access the context via the children prop as a callback function:

// task-list.js
export function TaskList() {
  return (
    <TodosConsumer>
      {({ todoList }) => (
        <ol className="task-list">
          {todoList.map((task, i) => {
            return (
              <li className="task-item" key={i}>
               {/* ... */}
              </li>
            );
          })}
        </ol>
      )}
    </TodosConsumer>
  );
}

Another option to control access to the consumer is to create a custom React hook using useContext, wrap some error logic like ‘expect todo provider to be present’. See code example below:

export function useToDo(){
  const context = React.useContext(TodosContext);
  if (context === undefined) {
    throw new Error('useToDo must be used within a TodoProvider')
  }

  return context
}

Explore my CodeSandbox further, Refactor ToDo app useReducer and React Context

I hope this helps you use React context more effectively in your day to day coding. Comment on Twitter or below.