DEV Community

Cover image for Practical Tips for Clean Code and Reduced Prop Drilling in React
Roman
Roman

Posted on • Originally published at programmingly.dev

Practical Tips for Clean Code and Reduced Prop Drilling in React

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.

prop drilling visual example

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>
      );
    };
Enter fullscreen mode Exit fullscreen mode

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>
    );
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode
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;
Enter fullscreen mode Exit fullscreen mode

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)