Почему мой код дважды извлекает одни и те же элементы при загрузке страницы?

Я изучаю React и пытаюсь получить данные для списка продуктов.

Предполагается, что сначала нужно получить только 2 элемента, а при нажатии кнопки — еще 2 элемента, но теперь при первой загрузке страницы одни и те же элементы извлекаются дважды.

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.info(result);
    } catch (e) {
      console.info(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>
  );
}

Я думал, что ошибка, возможно, связана с тем, что я поместил асинхронную функцию за пределы useEffect, но все равно не смог решить проблему, даже если поместил ее в useEffect.

может кто-нибудь подсказать, где может быть ошибка? Спасибо

🤔 А знаете ли вы, что...
JavaScript активно развивается, и новые версии языка регулярно включают новые функции и улучшения.


50
2

Ответы:

Решено

Скорее всего, вам нужно удалить <React.StrictMode> из вашего файла index.js. Возможный дубликат React Hooks: useEffect() вызывается дважды, даже если в качестве аргумента используется пустой массив


Как уже упоминалось, ваша проблема заключается в использовании <StrictMode>.

Примечание. Если вы не используете бесконечную прокрутку, вы можете разложить результаты по страницам и просто заменить все состояние products результатами выборки. Я чувствую, что это будет лучший подход.

Вы можете добавить проверку, чтобы узнать, добавлены ли уже добавляемые элементы:

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);
};

И затем выход короткого замыкания:

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

Фрагмент

Как вы можете видеть ниже, сообщение «Fetching...» печатается дважды, поскольку React дважды вызывает ваш эффект с помощью StrictMode. Вы можете защититься от двойного добавления, проверив, как описано выше.

Чтобы фрагмент работал, мне пришлось заменить async/await на традиционный обратный вызов обещания.

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.info(e);
      })
      .finally(() => {
        setLoading(false);
      });
  }

  useEffect(() => {
    console.info('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>

Использование ссылки

Более идиоматический способ справиться с этим — использовать ссылку.

const lastFetchedIdsRef = useRef([]);

И проверяем совпадение:

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];

Вот обновленный фрагмент:

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.info(e);
      })
      .finally(() => {
        setLoading(false);
      });
  }

  useEffect(() => {
    console.info('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>

Интересные вопросы для изучения