Я изучаю 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 активно развивается, и новые версии языка регулярно включают новые функции и улучшения.
Скорее всего, вам нужно удалить <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>