DEV Community

Cover image for Improving State Management in React: Transitioning from Prop Drilling to ContextAPI
Abinash Ray Yadav
Abinash Ray Yadav

Posted on

Improving State Management in React: Transitioning from Prop Drilling to ContextAPI

Whenever people start learning React, they inevitably encounter the challenge of handling props. Props are essential for passing data from parent to child components, enabling the creation of dynamic and reusable UI elements. However, as applications grow in complexity, developers often face a common hurdle known as prop drilling. This occurs when props must traverse multiple intermediary components to reach deeply nested children. In this article, we will explore the pitfalls of prop drilling and discuss how modern state management solutions, such as Recoil, can simplify and enhance your React development experience.

Let's begin with a product list prop drilling example:

Product Props drilling

Components Breakdown:
App Component:

function Home() {
  const [data, setData] = useState<Array<Product>>([]);
  const [loading, setLoading] = useState<boolean>(true);

  async function loadData()
  {
    const res = await fetch("https://fakestoreapi.com/products");
    let json = await res.json();
    json = json.slice(0, 3);
    setData(json);
    setLoading(false);
  }

  useEffect(() => {
    loadData();
  }, []);

  return (
  <main className="min-h-screen flex items-center justify-center">
    {loading ? <div>Loading...</div> : <ShoppingList products={data}/>}
  </main>);
}
Enter fullscreen mode Exit fullscreen mode

Shopping list Component:

function ShoppingList({products}: {products: Array<Product>})
{
    return (
        <div className="flex gap-3 items-stretch">
            {products && products.map(product=> <ProductCard product={product}/>)}
        </div>
        )
}
Enter fullscreen mode Exit fullscreen mode

Product Component:

function ProductCard({product}: {product: Product}){
  return (
  <div>
      <div className="bg-white border border-yellow-700 rounded-lg shadow dark:bg-gray-800 dark:border-gray-700 w-72 h-96">
                  <img className="py-8 rounded-lg h-52 pl-20" src={product.image} alt="product image" />

              <div className="px-5">
                      <h5 className="text-xl font-semibold tracking-tight text-gray-900 dark:text-white">{product.title}</h5>


                  <div className="flex items-center mt-2.5 mb-5">
                      <Star rating={product.rating.rate}/>
                      <span className="bg-blue-100 text-blue-800 text-xs font-semibold px-2.5 py-0.5 rounded dark:bg-blue-200 dark:text-blue-800 ms-3">
                          {product.rating.rate}
                          </span>
                  </div>


                  <div className="flex items-center justify-between">
                      <span className="text-3xl font-bold text-gray-900 dark:text-white">${product.price}</span>
                      <div className="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800">Add to cart</div>
                  </div>
              </div>
      </div>
  </div>)
}

Enter fullscreen mode Exit fullscreen mode

Star Component

function Star({rating}: {rating: number}){
  let noRating = 5 - Math.floor(rating);
  return (
      <>
          <div className="flex items-center space-x-1 rtl:space-x-reverse">


              {Array.from({ length: rating }).map((_, index) => (<svg className="w-4 h-4 text-yellow-300" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 22 20">
                  <path d="M20.924 7.625a1.523 1.523 0 0 0-1.238-1.044l-5.051-.734-2.259-4.577a1.534 1.534 0 0 0-2.752 0L7.365 5.847l-5.051.734A1.535 1.535 0 0 0 1.463 9.2l3.656 3.563-.863 5.031a1.532 1.532 0 0 0 2.226 1.616L11 17.033l4.518 2.375a1.534 1.534 0 0 0 2.226-1.617l-.863-5.03L20.537 9.2a1.523 1.523 0 0 0 .387-1.575Z"/>
              </svg>))}

              {Array.from({ length: noRating }).map((_, index) => (<svg className="w-4 h-4 text-gray-200 dark:text-gray-600" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 22 20">
                  <path d="M20.924 7.625a1.523 1.523 0 0 0-1.238-1.044l-5.051-.734-2.259-4.577a1.534 1.534 0 0 0-2.752 0L7.365 5.847l-5.051.734A1.535 1.535 0 0 0 1.463 9.2l3.656 3.563-.863 5.031a1.532 1.532 0 0 0 2.226 1.616L11 17.033l4.518 2.375a1.534 1.534 0 0 0 2.226-1.617l-.863-5.03L20.537 9.2a1.523 1.523 0 0 0 .387-1.575Z"/>
              </svg>))}
          </div>
      </>
  )
}
Enter fullscreen mode Exit fullscreen mode

This leads to unnecessary re-renders of components involving props and impact performance. Sometimes passing props to component without context of usage leads to debugging more difficult, as it is harder to trace the flow of data through the component tree.

To address the issues associated with prop drilling, several alternatives can be used and React Context API allows you to create a context and share data across the component tree without passing props explicitly at every level.

Setting up for ContextAPI:

1. Creating context

import { createContext } from "react";
import { Product } from "../types/types.next";

export const ProductContext = createContext<Product[] | null>(null);
Enter fullscreen mode Exit fullscreen mode

2. Wrap the Context Provider around Parent div

export default function Home() {
  const [data, setData] = useState<null | Product[]>(null);
  const [loading, setLoading] = useState<boolean>(true);

  async function loadData()
  {
    const res = await fetch("https://fakestoreapi.com/products");
    let json = await res.json();
    json = json.slice(0, 3);
    setData(json);
    setLoading(false);
  }

  useEffect(() => {
    loadData();
  }, []);

  return (
  <main className="min-h-screen flex items-center justify-center">
    {loading ? <div>Loading...</div> : <ProductContext.Provider value={data}> <ShoppingList/> </ProductContext.Provider>}
  </main>);
}
Enter fullscreen mode Exit fullscreen mode

3.Use useContext hook to use data

function ShoppingList()
{
  const products = useContext(ProductContext);
    return (
        <div className="flex gap-3 items-stretch">
            {products && products.map(product=> <ProductCard product={product}/>)}
        </div>
        )
}

Enter fullscreen mode Exit fullscreen mode

That's how we got global point of management for our props facilitating simplified component communication. But this comes with costs. Context providers trigger a re-render in all consuming components whenever the context value changes, regardless of whether the component actually uses that part of the context. This can lead to unnecessary re-renders and impact performance, especially if the context value changes frequently.

It leads to path for consideration of dedicated state management library like Redux or Recoil might better suit your application's needs, especially for complex global state management scenarios.

Top comments (0)