skip to Main Content

I am learning React and trying to fetch some data for a product list.

It is supposed to fetch only 2 items at first and fetch 2 more items when pressing the button, but it is now fetching the same items twice when the page is loading at first.

import { useEffect, useState, Fragment } from 'react';
import './style.css';

export default function LoadMoreData() {
  const [loading, setLoading] = useState(false);
  const [products, setProducts] = useState([]);
  const [count, setCount] = useState(0);
  const [disableButton, setDisableButton] = useState(false);

  const loadLimit = 10;
  let limit = 2;

  async function fetchProducts() {
    const dataUrl = `https://dummyjson.com/products?limit=${limit}&skip=${
      count === 0 ? 0 : count * limit
    }`;

    try {
      setLoading(true);
      const response = await fetch(dataUrl);
      const result = await response.json();

      if (result && result.products && result.products.length) {
        setProducts((prevData) => [...prevData, ...result.products]);
        setLoading(false);
      }

      console.log(result);
    } catch (e) {
      console.log(e);
      setLoading(false);
    }
  }

  useEffect(() => {
    fetchProducts();
  }, [count]);

  useEffect(() => {
    if (products && products.length === loadLimit) setDisableButton(true);
  }, [products]);

  if (loading) {
    return <div>Loading data! Please wait.</div>;
  }

  return (
    <Fragment>
      <div className='load-more-container'>
        <div className='product-container'>
          {products && products.length
            ? products.map((item) => (
                <div className='product' key={item.id}>
                  <img src={item.thumbnail} alt={item.title} />
                  <p>{item.title}</p>
                  <span>$ {item.price}</span>
                </div>
              ))
            : null}
        </div>
        <div className='button-container'>
          <button disabled={disableButton} onClick={() => setCount(count + 1)}>
            Load more products
          </button>
          {disableButton ? (
            <p>You have reached to {loadLimit} products!.</p>
          ) : null}
        </div>
      </div>
    </Fragment>
  );
}

I thought the bug maybe because I put the async function outside the useEffect, but still couldn’t solve the problem even though put it in the useEffect

can anyone give me a hint where the bug would be? Thank you

2

Answers


  1. You most likely need to remove <React.StrictMode> from your index.js file.
    Possible duplicate of React Hooks: useEffect() is called twice even if an empty array is used as an argument

    Login or Signup to reply.
  2. As mentioned, your issue is the <StrictMode> usage.

    Note: Unless you are using an infinite scroll, you could page your results and just replace the entire products state with the results of the fetch. I feel like this would be a better approach.

    You can add a check to see if the items to be appended have already been appended:

    const isOldLastSameAsNewLast = (prev, curr) => {
      const prevSlice = prev.slice(-curr.length)
      if (prevSlice.length !== curr.length) return false;
      return curr.every((e, i) => e.id === prevSlice[i].id);
    };
    

    And then short-circuit exit:

    if (isOldLastSameAsNewLast(prevData, result.products)) return prevData
    

    Snippet

    As you can see below, the "Fetching…" message is printed twice, because React calls your effect twice with StrictMode. You can guard against appending twice by checking as described above.

    For the snippet to work, I had to change the async/await to a traditional promise callback.

    const { Fragment, StrictMode, useEffect, useState } = React;
    
    const isOldLastSameAsNewLast = (prev, curr) => {
      const prevSlice = prev.slice(-curr.length)
      if (prevSlice.length !== curr.length) return false;
      return curr.every((e, i) => e.id === prevSlice[i].id);
    };
    
    const LoadMoreData = () => {
      const [loading, setLoading] = useState(false);
      const [products, setProducts] = useState([]);
      const [count, setCount] = useState(0);
      const [disableButton, setDisableButton] = useState(false);
    
      const loadLimit = 10;
      let limit = 2;
    
      function fetchProducts() {
        const dataUrl = `https://dummyjson.com/products?limit=${limit}&skip=${count * limit}`;
    
        setLoading(true);
    
        fetch(dataUrl)
          .then(response => response.json())
          .then(result => {
            if (!result || !result.products || !result.products.length) return;
            setProducts((prevData) => {
              if (isOldLastSameAsNewLast(prevData, result.products)) return prevData
              return [...prevData, ...result.products]; // Append
            });
          })
          .catch(e => {
            console.log(e);
          })
          .finally(() => {
            setLoading(false);
          });
      }
    
      useEffect(() => {
        console.log('Fetching...');
        fetchProducts();
      }, [count]);
    
      useEffect(() => {
        if (products && products.length === loadLimit) setDisableButton(true);
      }, [products]);
    
      if (loading) {
        return <div>Loading data! Please wait.</div>;
      }
    
      return (
        <Fragment>
          <div className='load-more-container'>
            <div className='product-container'>
              {products && products.length
                ? products.map((item) => (
                    <div className='product' key={item.id}>
                      <img src={item.thumbnail} alt={item.title} />
                      <p>{item.title}</p>
                      <span>$ {item.price}</span>
                    </div>
                  ))
                : null}
            </div>
            <div className='button-container'>
              <button disabled={disableButton} onClick={() => setCount(count + 1)}>
                Load more products
              </button>
              {disableButton ? (
                <p>You have reached to {loadLimit} products!.</p>
              ) : null}
            </div>
          </div>
        </Fragment>
      );
    };
    
    ReactDOM.createRoot(document.getElementById("root")).render(
      <StrictMode>
        <LoadMoreData />
      </StrictMode>
    );
    <div id="root"></div>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.development.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.development.js"></script>

    Using a ref

    A more idiomatic way to handle this would be to use a ref.

    const lastFetchedIdsRef = useRef([]);
    

    And checking for an overlap:

    const newProductIds = result.products.map(p => p.id);
    const hasOverlap = newProductIds.some(id => lastFetchedIdsRef.current.includes(id));
    if (hasOverlap) return;
    
    setProducts((prevData) => [...prevData, ...result.products]);
    lastFetchedIdsRef.current = [...lastFetchedIdsRef.current, ...newProductIds];
    

    Here is the updated snippet:

    const { Fragment, StrictMode, useEffect, useState, useRef } = React;
    
    const LoadMoreData = () => {
      const [loading, setLoading] = useState(false);
      const [products, setProducts] = useState([]);
      const [count, setCount] = useState(0);
      const [disableButton, setDisableButton] = useState(false);
    
      const lastFetchedIdsRef = useRef([]);
      const loadLimit = 10;
      let limit = 2;
    
      function fetchProducts() {
        const dataUrl = `https://dummyjson.com/products?limit=${limit}&skip=${count * limit}`;
    
        setLoading(true);
    
        fetch(dataUrl)
          .then(response => response.json())
          .then(result => {
            if (!result || !result.products || !result.products.length) return;
            const newProductIds = result.products.map(p => p.id);
            const hasOverlap = newProductIds.some(id => lastFetchedIdsRef.current.includes(id));
            if (hasOverlap) return;
            
            setProducts((prevData) => [...prevData, ...result.products]);
            lastFetchedIdsRef.current = [...lastFetchedIdsRef.current, ...newProductIds];
          })
          .catch(e => {
            console.log(e);
          })
          .finally(() => {
            setLoading(false);
          });
      }
    
      useEffect(() => {
        console.log('Fetching...');
        fetchProducts();
      }, [count]);
    
      useEffect(() => {
        if (products.length >= loadLimit) {
          setDisableButton(true);
        }
      }, [products]);
    
      if (loading) {
        return <div>Loading data! Please wait.</div>;
      }
    
      return (
        <Fragment>
          <div className='load-more-container'>
            <div className='product-container'>
              {products.map((item) => (
                <div className='product' key={item.id}>
                  <img src={item.thumbnail} alt={item.title} />
                  <p>{item.title}</p>
                  <span>$ {item.price}</span>
                </div>
              ))}
            </div>
            <div className='button-container'>
              <button disabled={disableButton} onClick={() => setCount(count + 1)}>
                Load more products
              </button>
              {disableButton && <p>You have reached to {loadLimit} products!.</p>}
            </div>
          </div>
        </Fragment>
      );
    };
    
    ReactDOM.createRoot(document.getElementById("root")).render(
      <StrictMode>
        <LoadMoreData />
      </StrictMode>
    );
    <div id="root"></div>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.development.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.development.js"></script>
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search