Like other React Developers, you often find yourself dealing with the common challenge of managing state and data flow efficiently.
One common issue that came in is Prop Drilling, where data is being passed from parent to child and deeply nested child components making all the components depend on each other. You can learn more about Props in React here.
It makes our code hard to manage, components are no longer reusable, and leads to bad code.
In this article, I’ll share some practical tips and tricks for writing cleaner code and avoiding prop drilling with code examples.
What is Prop Drilling?
The term Prop Drilling often means, when we want specific data from a parent component to a deeply nested component tree, and that data is being passed from each component in that tree and reaches a destination component.
This practice led us to complexity, hard to maintain, and made our components fully depend on each other which also destroyed the re-useability of components.
Take a look at the below image, the blue boxes represent the components hierarchy tree that passes the same props for the destination component.
Example Scenario:
Imagine you’re building an e-commerce application where users can browse products, view details, and add to a cart. The product data passed through each component from the parent to the deeply nested child component to serve product data. Here is an example:
// Top-level component
const ProductPage = () => {
const products = [
{ id: 1, name: 'Product 1', price: '$10' },
{ id: 2, name: 'Product 2', price: '$20' },
];
return (
<ProductList products={products} />
);
};
// Intermediate component
const ProductList = ({ products }) => {
return (
<ul>
{products.map(product => (
<ProductCard key={product.id} product={product} />
))}
</ul>
);
};
// Deeply nested component
const ProductCard = ({ product }) => {
return (
<li>
<h2>{product.name}</h2>
<p>{product.price}</p>
<ProductDetails product={product} />
</li>
);
};
// Deeply nested component
const ProductDetails = ({ product }) => {
return (
<div>
<h3>{product.name}</h3>
<p>{product.price}</p>
<p>Details: Detailed description about the product.</p>
</div>
);
};
If you see this example code, you’ll find it similar to the above image, where product data passed from ProductPage
to ProductList
then ProductCard
and reaches the destination component at ProductDetails
.
How to avoid Prop Drilling and writing Clean Code?
In the above example, you’ll see how data is drilled down from parent to deeply nested child component, which is now a bad practice and leads to so many bugs and is hard to maintain.
The actual solution proposed for this problem is to make the state global. This is what we call separation of concern, in which functionality is kept separate from the UI and used whenever it is needed.
To solve the prop drilling problem and implement the global state management solution, Here are some solutions with an example:
1) Context API
The Context API in React provides a way to share state to multiple components without passing the props through each component.
In Context API, functionality is managed in one place and the same data is consumed by only those components that need it.
See this example below to understand how Context API solves this problem with a simpler more easier solution:
// Create a context for the product
const ProductContext = React.createContext();
const ProductProvider = ({ children, products }) => (
<ProductContext.Provider value={products}>
{children}
</ProductContext.Provider>
);
// Top-level component
const ProductPage = () => {
const products = [
{ id: 1, name: 'Product 1', price: '$10' },
{ id: 2, name: 'Product 2', price: '$20' },
];
return (
<ProductProvider products={products}>
<ProductList />
</ProductProvider>
);
};
// Intermediate component
const ProductList = () => (
<ProductContext.Consumer>
{products => (
<ul>
{products.map(product => (
<ProductCard key={product.id} product={product} />
))}
</ul>
)}
</ProductContext.Consumer>
);
// Deeply nested component
const ProductCard = ({ product }) => (
<li>
<h2>{product.name}</h2>
<p>{product.price}</p>
<ProductDetails />
</li>
);
const ProductDetails = () => (
<ProductContext.Consumer>
{products => {
const product = products.find(p => p.id === 1); // Example to find a specific product
return (
<div>
<h3>{product.name}</h3>
<p>{product.price}</p>
<p>Details: Detailed description about the product.</p>
</div>
);
}}
</ProductContext.Consumer>
);
In this example, ProductContext
is used for sharing product data to each component where needed without drilling the props deeply.
2) Component Composition
Another great way to avoid prop drilling is by using the react best practice that involves the creation of nested smaller but reusable components to build complex UI. This is like the atomic design pattern i have explained previously where we think of each element as an atom and combine them to build widgets, modules, and so on.
This method helps to logically group related data and functionality. It helps us write cleaner code and reduce the need for prop drilling too.
By using Component Composition, imagine a user wants to see its info and orders on the User Dashboard Page.
Here is how we can implement this example using Component Composition:
import React from "react";
// Mock Data
const user = {
name: "Alice Doe",
email: "alice@example.com",
};
const orders = [
{ id: 1, item: "Laptop", price: "$999", status: "Delivered" },
{ id: 2, item: "Headphones", price: "$199", status: "Shipped" },
{ id: 3, item: "Smartphone", price: "$799", status: "Processing" },
];
// Main Dashboard Component
const Dashboard = () => {
return (
<div>
<h1>User Dashboard</h1>
{/* Compose User Profile and Order List */}
<div>
<UserProfile user={user} />
<OrderHistory orders={orders} />
</div>
</div>
);
};
// User Profile Component
const UserProfile = ({ user }) => (
<div>
<h2>User Profile</h2>
<p><strong>Name:</strong> {user.name}</p>
<p><strong>Email:</strong> {user.email}</p>
</div>
);
// Order History Component
const OrderHistory = ({ orders }) => (
<div>
<h2>Order History</h2>
<ul>
{orders.map((order) => (
<OrderItem key={order.id} order={order} />
))}
</ul>
</div>
);
// Order Item Component
const OrderItem = ({ order }) => (
<li>
<p><strong>Item:</strong> {order.item}</p>
<p><strong>Price:</strong> {order.price}</p>
<p><strong>Status:</strong> {order.status}</p>
</li>
);
export default Dashboard;
3) Extract State Logic into Custom Hook
In the above example, you see how data can be managed globally in one place in Context API.
The only difference between custom hook way and Context API is Context is used to manage globally and components must be wrapped with Providers to share data.
But Custom Hooks work well with less data, like Fetching User Data and servers from one place. Here data is centralized but no wrapper is needed.
Whole logic is maintained in one place which is shared across different components to avoid prop drilling and keep code clean and non-repetitive.
Here is how we can achieve the same result with Custom Hook by extending Component Composition:
1) Creating Custom Hook:
// Custom Hook
import { useState } from "react";
// Custom Hook: useDashboardData
const useDashboardData = () => {
const [user, setUser] = useState({
name: "Alice Doe",
email: "alice@example.com",
});
const [orders, setOrders] = useState([
{ id: 1, item: "Laptop", price: "$999", status: "Delivered" },
{ id: 2, item: "Headphones", price: "$199", status: "Shipped" },
{ id: 3, item: "Smartphone", price: "$799", status: "Processing" },
]);
return { user, orders, setUser, setOrders };
};
export default useDashboardData;
2) Sharing data between different Components:
import React from "react";
import useDashboardData from "./useDashboardData"; // Import Custom Hook
const Dashboard = () => {
const { user, orders } = useDashboardData(); // State and data from the custom hook
return (
<div>
<h1>User Dashboard</h1>
<div>
<UserProfile user={user} />
<OrderHistory orders={orders} />
</div>
</div>
);
};
const UserProfile = ({ user }) => (
<div>
<h2>User Profile</h2>
<p>
<strong>Name:</strong> {user.name}
</p>
<p>
<strong>Email:</strong> {user.email}
</p>
</div>
);
const OrderHistory = ({ orders }) => (
<div>
<h2>Order History</h2>
<ul>
{orders.map((order) => (
<OrderItem key={order.id} order={order} />
))}
</ul>
</div>
);
const OrderItem = ({ order }) => (
<li>
<p>
<strong>Item:</strong> {order.item}
</p>
<p>
<strong>Price:</strong> {order.price}
</p>
<p>
<strong>Status:</strong> {order.status}
</p>
</li>
);
export default Dashboard;
Wrapping thing
React can be an amazing library, we can create or break things. But with the right knowledge and best practices, we can build amazing apps by writing clean code and managing the state very easily.
In this article, I tried to explain 3 different ways to avoid prop drilling problems along with best practices for writing clean code.
By focusing on these methods we’ll not only improve the performance of applications but also make the development process more efficient and enjoyable.
What method do you like the most? or which one you’re already using? (Share your thoughts in the comments)
This Blog Originally Posted at Programmingly.dev. Understand & Learn Web by Joining our Newsletter for Web development & Design Articles
Top comments (0)