skip to Main Content

In my App, I am implementing Product Listing page which lists out the products on the listing page. I am looking to integrate some good practices based on this post – https://dev.to/vyan/using-async-functions-in-useeffect-best-practices-and-pitfalls-o75 but instead of let I am using ref to track.

I am getting the list of products in the console message but it displays no list in the UI. When I comment the – isMounted.current = false; line in my cleanup function then only it displays the list. Let me know what I am doing wrong here with the cleanup ?

App.jsx –

import { useEffect, useRef, useState } from "react";
import ProductsList from "./components/ProductsList";
import "./styles.css";

export default function App() {
  const isMounted = useRef(true);
  const [products, setProducts] = useState([]);

  const fetchData = async () => {
    try {
      const data = await fetch("https://dummyjson.com/products");
      const { products = [] } = await data.json();
      console.log(products); // It shown as Array of object in the console

      if (isMounted.current) {
        setProducts(products);
      }
    } catch (error) {
      if (isMounted.current) {
        console.error("Error fetching data: ", error);
      }
    }
  };

  useEffect(() => {
    fetchData();
    return () => {
      isMounted.current = false; // Cleanup on unmount
    };
  }, []);

  return (
    <div className="App">
      <h1>Product Listing Page</h1>
      {products.length ? <ProductsList products={products} /> : null}
    </div>
  );
}

ProductsList.jsx –

const ProductsList = ({ products }) => {
  return (
    <div className="products-list">
      {products.map((product) => {
        return (
          <div className="product" key={product.id}>
            <div className="max-w-sm rounded overflow-hidden shadow-lg">
              <img
                className="w-full"
                src={product.thumbnail}
                alt={product.thumbnail}
              />
              <div className="px-6 py-4">
                <div className="font-bold text-xl mb-2">{product.title}</div>
                <p className="text-gray-700 text-base">{product.description}</p>
              </div>
              <div className="px-6 pt-4 pb-2">
                {product.tags.map((tag, index) => (
                  <span
                    key={index}
                    className="inline-block bg-gray-200 rounded-full px-3 py-1 text-sm font-semibold text-gray-700 mr-2 mb-2"
                  >
                    #{tag}
                  </span>
                ))}
              </div>
            </div>
          </div>
        );
      })}
    </div>
  );
};

export default ProductsList;

2

Answers


  1. In React Strict Mode, React invokes your effect setup and cleanup function (simulating a mount/umount) twice. Because of this, when the setup function is reinvoked for the second time, the ref value that was updated in the cleanup function would still be set to false (see this discussion on whether this is intended behaviour).

    The React team discourage using refs to detect mounts and also suggests using variables as mentioned in your article to determine if the component has unmounted.

    If you must use refs, you can reset your ref to true on the initial mount (essentially "undoing" the effects from the strict-mode cleanup call):

    useEffect(() => {
      isMounted.current = true;
      fetchData();
      return () => {
        isMounted.current = false; // Cleanup on unmount
      };
    }, []);
    

    However, I’d suggest you try with the variable approach as suggest in the docs to avoid this pattern.

    Login or Signup to reply.
  2. To Further explain the answer above, In React’s Strict Mode the setup and cleanup functions of effects are invoked twice on mount and unmount to help identify side effects that might not clean up properly. So this means that when you use a ref to track whether the component is mounted or not, the ref can be set to false during cleanup. If your effect runs again (due to the double invocation), the ref will still be false, which could prevent your data-fetching logic from updating the state.

    Using a Variable Approach
    Instead of relying on a ref to track whether a component is mounted, you can use a simple variable within the effect. Here’s how you can implement this:

    
    import { useEffect, useState } from "react";
    import ProductsList from "./components/ProductsList";
    import "./styles.css";
    
    export default function App() {
      const [products, setProducts] = useState([]);
    
      const fetchData = async () => {
        let isMounted = true; // Declare a variable to track the mount status
        try {
          const response = await fetch("https://dummyjson.com/products");
          const data = await response.json();
          console.log(data.products); // Log the fetched products
    
          // Only update state if the component is still mounted
          if (isMounted) {
            setProducts(data.products || []);
          }
        } catch (error) {
          console.error("Error fetching data: ", error);
        }
        return () => {
          isMounted = false; // Set the variable to false on cleanup
        };
      };
    
      useEffect(() => {
        fetchData(); // Call fetchData which includes the isMounted variable
    
        // Cleanup function doesn't need to do anything since isMounted is scoped within fetchData
        return () => {};
      }, []);
    
      return (
        <div className="App">
          <h1>Product Listing Page</h1>
          {products.length ? <ProductsList products={products} /> : <p>Loading...</p>}
        </div>
      );
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search