Prop Drilling In React
In this article, I'll be going through a React concept that many beginners have trouble dealing with as they may be unaware that this is something they shouldn't be doing or, better still, a bad practice. This concept is known as prop drilling. It is a concept every React developer must know about, as we sometimes face scenarios where we must pass data from a top-level component down to another component. Determining the best way to do so is critical. So let’s jump right in!
What is prop drilling?
Prop drilling, also known as "prop passing," is a technique in React for passing data through several nested children components to deliver this data to a deeply nested component.
Imagine you have an application where you have an app component, e.g. app.js
, which creates a state, and then you have two more components in which one of the components is called inside the app components, and then that component calls another component that is below it.
The problem with prop drilling is that it can become cumbersome and difficult to manage and keep track of as the number of components in a tree structure increases. This is because data must be passed through each component in the tree, even if only a single component at the bottom of the tree needs that data.
Additionally, when the component tree changes, such as when new components are added or existing components are removed, the flow of data may also need to be adjusted, which can become time-consuming and error-prone. It can make adding new features, fixing bugs, and making other application changes harder.
Let’s see an example with the illustration we used above.
Example
In our example below, We have our app.js
, which is the parent component, and it creates the state called count
that increases each time we click the button. We created two more components inside this: our Button
component and DisplayCount
application.
App.js:
//app.js
import logo from "./logo.svg";
import "./App.css";
import { useState } from "react";
import DisplayCount from "./DisplayCount";
function App() {
const [count, setCount] = useState(0);
return (
<div className="App">
<DisplayCount count={count} setCount={setCount} />
</div>
);
}
export default App;
DisplayCount.js:
//display count
import React from "react";
import Button from "./Button";
function DisplayCount(props) {
return (
<div>
{props.count}
<Button setCount={props.setCount} />
</div>
);
}
export default DisplayCount;
Button.js:
// button
import React,{useContext} from "react";
import { countContext } from "./Context";
function Button(props) {
return (
<button
onClick={() => {
props.setCount((count) => count + 1);
}}
>
increase button
</button>
);
}
export default Button;
Notice we created the count state on the app component, and we are passing it as props to the display count component so we can pass it down to our components, which works.
To achieve our count increasing each time the button is clicked, we need to access our setCount
inside our button component, so what we need to do is pass the setCount
state as props inside the displayCount
component, and from the display count
, we grab it and pass it as props to the one below which is our button component and then add an onClick
function to it. We are just increasing the value of count
whenever we click the button.
Now the problem with this is that our setCount
function is created in the app component, but we have to pass it as props to the displayCount
component even though it’s not using it or doing anything with it other than passing it as props down to the components that need it. If it were to be in a large-scale application, this approach would be cumbersome and redundant and also difficult to maintain.
Ways to avoid prop drilling
Some of the ways to avoid prop drilling are:
- Using the context API: The context API allows a component to share data with any of its child components without the need to pass props through every component in the tree.
Component composition: Component composition is a method of passing components as props to other components.
Using a state management library such as Redux or Mobx: These libraries provide a centralized store for the application state, which any component in the application can access.
Using a Higher-Order Component (HOC) or Render Props: A HOC is a component that wraps another component and provides it with additional props. A Render Props is a component that accepts a function as a prop and calls it with its state and any other data.
Use of hooks like
useContext
,useReducer
, anduseState
.
Let’s take a look at two of these solutions, which are
Context API
Component composition
Context API
The Context API is a feature in React that allows developers to share states and methods across a component tree without having to pass props through every level of the tree.
It works by creating a context object that holds the shared state and methods and then providing that context to the components that need it through a provider component. The components that need access to the shared state and methods can then consume the context through a consumer component.
Modifying our Count example to use the context API:
App.js:
// app.js
import React, { createContext, useState } from 'react';
import DisplayCount from './displaycount';
const CountContext = createContext();
function App() {
const [count, setCount] = useState(0);
return (
<CountContext.Provider value={{ count, setCount }}>
<div className="App">
<DisplayCount />
</div>
</CountContext.Provider>
);
}
export default App;
DisplayCount.js:
// DisplayCount
import React, { useContext } from 'react';
import { CountContext } from './app';
import Button from './button';
function DisplayCount() {
const { count } = useContext(CountContext);
return (
<div>
<h1>Count: {count}</h1>
<Button />
</div>
);
}
export default DisplayCount;
Button.js:
//Button.js
import React, { useContext } from 'react';
import { CountContext } from './app';
function Button() {
const { count, setCount } = useContext(CountContext);
return (
<div>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
export default Button;
In the example provided, the CountContext.Provider
component wraps the root of the component tree and provides the shared state count and the method setCount
to its children. The DisplayCount
and Button
components can retrieve the shared state and method from the nearest CountContext.Provider
without having to pass props through every level of the component tree.
This makes the code more readable and maintainable, as it eliminates the need to pass props through multiple levels of the component tree. Additionally, it allows the state and methods to be defined in a central location, making it easier to manage and understand the flow of data and behavior in the application
It is worth noting that the Context API has some limitations, such as it does not work well with component-based solutions, and it is also not recommended for passing down props to more than one level. But in cases like the one in the example, it is a great solution for managing prop drilling and passing state and methods across a component tree.
Component composition
Component composition is a technique in React that involves breaking down a user interface into smaller, reusable components. These components can then be combined to create a more complex user interface.
It is a way of building a user interface by composing small, reusable components, instead of building one large, monolithic component. It allows developers to create a clear and modular structure for the application, making it easier to understand, maintain, and extend.
Modifying our count example to use component composition, we have:
//app.js
import React, { useState } from 'react';
import DisplayCount from './DisplayCount';
function App() {
const [count, setCount] = useState(0);
return (
<div className="App">
<DisplayCount count={count} setCount={setCount} />
</div>
);
}
export default App;
In this example, the App component only renders the DisplayCount
component and passes the state and method to it as props.
// DisplayCount
import React from 'react';
import Button from './Button';
function DisplayCount({ count, setCount }) {
return (
<div>
<h1>Count: {count}</h1>
<Button count={count} setCount={setCount}/>
</div>
);
}
export default DisplayCount;
In the DisplayCount
component, it receives the props count and setCount
, it displays the count and imports the Button component, passes the count and setCount
props to it.
// button.js
import React from 'react';
function Button({ count, setCount }) {
return (
<div>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
export default Button;
In the Button
component, it receives the count and setCount
props from the DisplayCount
component. The component then renders a button that, when clicked, increments the count value by calling the setCount
function and passing it the new count value, which then triggers a re-render of the DisplayCount
component and updates the displayed count value.
The example provided uses component composition to solve this problem by passing the shared state and methods down to the child components as props. The parent component, App, holds the state and methods and passes them down to the child component, DisplayCount
.
The DisplayCount
component, in turn, passes the props to the Button component. This allows the child components to access the state and methods without having to pass props through every level of the component tree, making the code more readable and maintainable.
This approach allows for a more organized and readable code, as the flow of state and behavior is more explicit, and it is easier to trace and understand.
Conclusion
In this article, we talked about what prop drilling is and why it is a bad practice. We also talked about some ways to avoid prop drilling.
Prop drilling can make your codebase hard to maintain and reason about as your component tree grows. It's important to use the appropriate techniques to avoid prop drilling and make your codebase more manageable and maintainable.