An Introduction to React’s useContext Hook

Jump right past the middleman and bypass prop drilling with React’s Context API.

Anyone who’s ever coded anything beyond “Hello, World!” in React knows what a chore prop drilling can become.

Prop drilling refers to the practice of passing props down from a parent component to a nested subchild component, requiring travel through one or many intermediary components that don’t utilize the props data themselves. The Context API—which includes the popular useContext hook, one of the three “basic” hooks built into React along with useState and useEffect—aims to reduce the headache.

As we’ll see shortly, however, useContext doesn’t replace all prop drilling. It’s recommended mostly for passing down global props that are reused throughout the application, such as an object representing the current logged-in user or styling themes. If you find yourself passing the same props from a parent component to many different children, or from a parent directly to a deeply nested component, consider implementing useContext.

Let’s get into a barebones example. Pretend we have a to-do app with the current to-dos saved as an array of objects in the top-level App.js component. The to-dos are displayed within a TodosContainer (a grandchild of App), which is itself inside a ScheduleContainer (a child of App).

With prop drilling, you’d send the to-dos array to the ScheduleContainer, which wouldn’t do anything with it except keep passing it on down the line to the TodosContainer. The ScheduleContainer doesn’t need access to the to-dos array. It doesn’t care. Its only function in this regard is to serve as a link in the prop-drilling chain.

The Context API allows us to jump right over the ScheduleContainer instead. Here’s how it looks:

App.js (Top-Level Component)

import React, { createContext } from 'react'
import ScheduleContainer from './containers/ScheduleContainer'
export const TodosContext = createContext([])function App(){ const todos = [
{task: "Feed Dog", completed: false},
{task: "Buy Groceries", completed: false}
]
return (
<main className="App">
<TodosContext.Provider value={todos}>
<ScheduleContainer />
</TodosContext.Provider>
</main>
)
}
export default App;

OK, so what’s new here?

  1. Import createContext: This is part of the Context API and included with React.
  2. Use createContext: Initializes an object, which can be empty or include starter data. Export if needed to pass to another file.
  3. Wrap Children in Provider: We’re only giving context to one container in this example, but if there were other components nested inside <TodosContext.Provider>, they also would have access to the todos array.
  4. Provide a Value Prop: This is what puts data into the empty array that we initialized with createContext. You could be more specific here if you wanted; for example, you could set the value only to todos[1].completed.

ScheduleContainer.js (Child of App.js)

import React from 'react'
import TodosContainer from './TodosContainer'
export default function ScheduleContainer() {
return (
<section className="schedule-container">
<TodosContainer />
</section>
)
}

The ScheduleContainer doesn’t touch the to-dos array at all. It doesn’t import it, it doesn’t accept it as a prop, it doesn’t pass it to any children. We are skipping the middleman entirely when utilizing useContext. No drilling required!

TodosContainer.js (Child of ScheduleContainer.js)

import React, { useContext } from 'react'
import { TodosContext } from '../App'
export default function TodosContainer() {

const todos = useContext(TodosContext)
return (
<section className="todos-container">
<p>{todos[1].task}</p>
</section>
)
}

Inside any subcomponent that’s nested within a Provider, all you need to do to access the data is import the object created with createContext and hook into it with useContext.

  1. Import useContext: Included as part of the Context API library with React.
  2. Import Context Object: Bring in the TodosContext object created with createContext from App.js.
  3. Define New Variable with useContext: Declare a new variable and assign it to the useContext hook, which takes in the Context object from App.js as an argument.

The todos variable is now an array of objects, exactly the same as if we’d passed down the object manually from App.js through ScheduleContainer.js to TodosContainer.js with prop drilling. In the real world you’d probably map through it and create a series of TodoCard components, but for the sake of simplicity in this example I hard-coded a single <p> tag.

There you have it! As you can see, this process would get tedious if you tried to use the Context API to pass every single prop. Some drilling is still necessary. The useContext hook, however, is a great tool for refactoring and streamlining code, especially if you’re repeating yourself over and over to send global data to several different nested components.

This is only a basic introduction to the useContext hook. The entire Context API goes much deeper, including a separate wrapper that’s a counterpart to Provider: the Consumer. The useContext hook, released with React 16.8, replaces some of the old-style workflows, but it’s never a bad idea to further understand what’s happening under the hood. React’s official Context API docs are well worth a thorough read.

Happy hacking!

Full-stack developer. Full-stack adventurer.