Context API: the solution to "prop drilling"

React's useContext() Hook is a great way to clean up components that contain unnecessary props

Published May 28, 2022

The problem with React props

Components are great — they allow you to easily organize different parts of your application into separate files. If you have a child component that needs a piece of state, simply lift that state up to the nearest parent component, and then pass it down as a prop. Here's an example:


// App.js
import IncrementButton from "./IncrementButton";
import { useState } from "react";

const App = () => {
  const [count, setCount] = useState(0);

  return (
    <div className="container">
      <h1>{count}</h1>
      <div>
        <IncrementButton setCount={setCount} />
      </div>
    </div>
  );
};

export default App;
        

// IncrementButton.js
const IncrementButton = (props) => {
  const incrementHandler = () => {
    props.setCount((prevCount) => prevCount + 1);
  };
  return (
    <>
      <button onClick={incrementHandler}>+</button>
    </>
  );
};

export default IncrementButton;
        

...this is a simple application with two components: App and IncrementButton. The parent component (App) holds the count state, which is used to display the current number. The setCount function is passed down to the child component (IncrementButton) as a prop, where it can then control the state. This works great, but what if the application looks something like this instead...


// App.js
import IncrementInterface from "./IncrementInterface";
import { useState } from "react";

const App = () => {
  const [count, setCount] = useState(0);

  return (
    <div className="container">
      <h1>{count}</h1>
      <div>
        <IncrementInterface setCount={setCount} />
      </div>
    </div>
  );
};

export default App;
        

// IncrementInterface.js
import IncrementButton from "./IncrementButton";

const IncrementInterface = (props) => {
  return (
    <div>
      <p>CLICK TO INCREMENT: </p>
      <IncrementButton setCount={props.setCount} />
    </div>
  );
};

export default IncrementInterface;
        

// IncrementButton.js
const IncrementButton = (props) => {
  const incrementHandler = () => {
    props.setCount((prevCount) => prevCount + 1);
  };
  return (
    <>
      <button onClick={incrementHandler}>+</button>
    </>
  );
};

export default IncrementButton;
        

...this time, setCount is passed down as a prop to the IncrementInterface component, which then passes it down again to the IncrementButton, where it can finally be used. This is a problem. Why? Well, the IncrementInterface component has no need for the setCount prop — it's simply passing it down to the IncrementButton component. This process of sending props from a higher-level component to a lower-level component is referred to as "prop drilling".

As you might imagine, this can quickly get messy if there's a lot of components. For example, if component "E" needs a piece of state from component "A", it needs to be sent all the way down as a prop, going through "B", "C", and "D", before finally reaching "E". Wouldn't it be great if you could just pass that state directly from component "A" to "E"? Well, you can!


Using the Context API

The React Context API is the solution to prop drilling. In a nutshell, the Context API is just a structure that allows you to share data across all levels of your application. Using the Context API is simple, and I'll show how by using the example from earlier.

Earlier I stated that I want to be able to pass down the setCount function directly to the IncrementButton component, without going through the IncrementInterface component at all. With the Context API, this can easily be achieved. First, I'll create a new folder in my application called contexts, which will hold all my contexts. Inside this folder, I'll create a new file called countContext.js, which is where I'll create the context. Here's what my file looks like after the context is created:


// countContext.js
import { createContext } from "react";

export const countContext = createContext({});
        

...the createContext function is imported from React and initialized with an empty object. Then, it's exported as a variable called countContext. Now, this context can be used in the parent component, which in this case is App...


// App.js
import IncrementInterface from "./IncrementInterface";
import { countContext } from "./contexts/countContext";
import { useState } from "react";

const App = () => {
  const [count, setCount] = useState(0);

  return (
    <countContext.Provider value={{ setCount }}>
      <div className="container">
        <h1>{count}</h1>
        <div>
          <IncrementInterface />
        </div>
      </div>
    </countContext.Provider>
  );
};

export default App;
        

...first, the countContext variable is imported into the component. Once imported, it's used just like any other component, wrapping around everything. The Provider keyword provides the ability to access context changes. Next, the component is given a prop of value, which will hold an object of any pieces of data you want to access across your application. For my application, I want to be able to access setCount from anywhere. Now, every component in between the countContext tags (including children of those components) will have access to setCount. Since I want to use setCount in the IncrementButton component, I need to import the context in that component, along with the useContext() React Hook...


// IncrementButton.js
import { useContext } from "react";
import { countContext } from "./contexts/countContext";

const IncrementButton = (props) => {
  const { setCount } = useContext(countContext);

  const incrementHandler = () => {
    setCount((prevCount) => prevCount + 1);
  };
  return (
    <>
      <button onClick={incrementHandler}>+</button>
    </>
  );
};

export default IncrementButton;
        

...the useContext() Hook is used to create data that can be accessed throughout the component hierarchy without prop drilling. Using it is easy: just destructure the variable(s) you got from countContext, which in this case is only setCount. After that, it can be used anywhere in the IncrementButton component without the use of props.

Contexts aren't limited to just one component; if I wanted to, I could use setCount across different components. At the end of the day, the Context API should be used whenever you have data that needs to be accessed throughout different components. It makes your codebase cleaner and removes the need for prop drilling, making it great for state management. For a more complex application, you should probably use a library like Redux for state management, which is much more powerful.

If you want to learn more about context, I recommend reading React's official documentation on the topic.