← Назад к вопросам
Какие проблемы возникают при бесконечном scroll на React?
2.0 Middle🔥 221 комментариев
#React
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI3 апр. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Проблемы бесконечного скролла в React
Это сложная фича которая имеет много подводных камней. Разберём проблемы и решения.
Проблема 1: Memory Leak (утечка памяти)
// ПЛОХО - DOM растёт бесконечно
function InfiniteScroll() {
const [items, setItems] = useState([]);
const [page, setPage] = useState(1);
useEffect(() => {
window.addEventListener('scroll', handleScroll);
// ОШИБКА: никогда не удаляем слушателя!
// При размонтировании компонента слушатель остаётся
}, []);
const handleScroll = () => {
if (isNearBottom()) {
fetchMore();
}
};
const fetchMore = async () => {
const newItems = await fetch(`/api/items?page=${page}`);
setItems([...items, ...newItems]); // УТЕЧКА: items растёт бесконечно
setPage(page + 1);
};
return (
<div>
{items.map(item => <ItemComponent key={item.id} {...item} />)}
</div>
);
}
// ХОРОШО - правильно управляем слушателями
function InfiniteScroll() {
const [items, setItems] = useState([]);
const [page, setPage] = useState(1);
useEffect(() => {
const handleScroll = () => {
if (isNearBottom()) {
fetchMore();
}
};
window.addEventListener('scroll', handleScroll);
// ПРАВИЛЬНО: удаляем слушателя при размонтировании
return () => {
window.removeEventListener('scroll', handleScroll);
};
}, [page, items]);
const fetchMore = async () => {
const newItems = await fetch(`/api/items?page=${page}`);
setItems(prev => [...prev, ...newItems]);
setPage(prev => prev + 1);
};
return <div>{items.map(item => <ItemComponent key={item.id} {...item} />)}</div>;
}
Проблема 2: Множественные запросы (Race Condition)
// ПЛОХО - может отправить несколько запросов одновременно
function InfiniteScroll() {
const [items, setItems] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const handleScroll = () => {
if (isNearBottom() && !isLoading) { // НЕДОСТАТОЧНО!
fetchMore();
}
};
const fetchMore = async () => {
setIsLoading(true);
// ПРОБЛЕМА: если scroll быстро сработал дважды,
// оба запроса начнут одновременно
const response = await fetch(`/api/items`);
const newItems = await response.json();
setItems(prev => [...prev, ...newItems]);
setIsLoading(false);
};
}
// ХОРОШО - использруй flag для предотвращения
function InfiniteScroll() {
const [items, setItems] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const isFetchingRef = useRef(false);
const handleScroll = () => {
if (isNearBottom() && !isFetchingRef.current) {
fetchMore();
}
};
const fetchMore = async () => {
if (isFetchingRef.current) return; // двойная проверка
isFetchingRef.current = true;
setIsLoading(true);
try {
const response = await fetch(`/api/items`);
const newItems = await response.json();
setItems(prev => [...prev, ...newItems]);
} finally {
isFetchingRef.current = false;
setIsLoading(false);
}
};
}
Проблема 3: Производительность (Rendering Performance)
// ПЛОХО - перерендеривает ВСЕ элементы
function InfiniteScroll() {
const [items, setItems] = useState([]);
return (
<div>
{/* Каждый скролл перерендеривает все 1000 элементов! */}
{items.map(item => (
<div key={item.id}>
<h3>{item.title}</h3>
<p>{item.description}</p>
<img src={item.image} alt={item.title} />
</div>
))}
</div>
);
}
// ХОРОШО - виртуализируй список
import { FixedSizeList } from 'react-window';
function InfiniteScroll() {
const [items, setItems] = useState([]);
const Row = ({ index, style }) => (
<div style={style}>
<ItemComponent item={items[index]} />
</div>
);
return (
<FixedSizeList
height={600}
itemCount={items.length}
itemSize={35}
width="100%"
>
{Row}
</FixedSizeList>
);
}
// react-window только рендеривает видимые элементы
// Не все 10000 элементов сразу!
Проблема 4: Обнаружение конца страницы
// ПЛОХО - неточное обнаружение
function InfiniteScroll() {
const isNearBottom = () => {
return window.innerHeight + window.scrollY >= document.body.offsetHeight;
};
// ПРОБЛЕМА: может не сработать на Edge cases
}
// ХОРОШО - используй Intersection Observer
function InfiniteScroll() {
const [items, setItems] = useState([]);
const bottomRef = useRef(null);
useEffect(() => {
// Intersection Observer более надёжен
const observer = new IntersectionObserver(
entries => {
if (entries[0].isIntersecting) {
fetchMore();
}
},
{ threshold: 0.1 }
);
if (bottomRef.current) {
observer.observe(bottomRef.current);
}
return () => {
if (bottomRef.current) {
observer.unobserve(bottomRef.current);
}
};
}, []);
return (
<div>
{items.map(item => <ItemComponent key={item.id} item={item} />)}
<div ref={bottomRef}>Загружаю ещё...</div>
</div>
);
}
Проблема 5: Дублирование элементов
// ПЛОХО - может быть дублирование
function InfiniteScroll() {
const [items, setItems] = useState([]);
const fetchMore = async () => {
const newItems = await fetch('/api/items');
// ПРОБЛЕМА: если запрос вернулся в неправильном порядке
// может быть дублирование
setItems([...items, ...newItems]);
};
}
// ХОРОШО - отслеживай последний ID
function InfiniteScroll() {
const [items, setItems] = useState([]);
const [lastId, setLastId] = useState(null);
const fetchMore = async () => {
const newItems = await fetch(`/api/items?after=${lastId}`);
setItems(prev => [...prev, ...newItems]);
setLastId(newItems[newItems.length - 1]?.id);
};
}
Проблема 6: Scroll Position потеряется
// ПЛОХО - скролл сбросится при обновлении
function InfiniteScroll() {
const [items, setItems] = useState([]);
// При каждом обновлении items скролл может сбросится
return <div>{items.map(item => <Item key={item.id} item={item} />)}</div>;
}
// ХОРОШО - сохраняй scroll position
function InfiniteScroll() {
const [items, setItems] = useState([]);
const containerRef = useRef(null);
const scrollPosRef = useRef(0);
const handleScroll = (e) => {
scrollPosRef.current = e.target.scrollTop;
};
const fetchMore = async () => {
const currentScroll = scrollPosRef.current;
const newItems = await fetch('/api/items');
setItems(prev => [...prev, ...newItems]);
// Восстанавливаем scroll position
setTimeout(() => {
if (containerRef.current) {
containerRef.current.scrollTop = currentScroll;
}
}, 0);
};
return (
<div ref={containerRef} onScroll={handleScroll}>
{items.map(item => <Item key={item.id} item={item} />)}
</div>
);
}
Проблема 7: Network errors
// ПЛОХО - не обработали ошибку
function InfiniteScroll() {
const fetchMore = async () => {
const response = await fetch('/api/items');
const newItems = await response.json();
setItems(prev => [...prev, ...newItems]);
};
}
// ХОРОШО - обработай ошибки
function InfiniteScroll() {
const [items, setItems] = useState([]);
const [error, setError] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const fetchMore = async () => {
setIsLoading(true);
setError(null);
try {
const response = await fetch('/api/items');
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const newItems = await response.json();
setItems(prev => [...prev, ...newItems]);
} catch (err) {
setError(err.message);
console.error('Ошибка загрузки:', err);
} finally {
setIsLoading(false);
}
};
return (
<div>
{error && <div className="error">Ошибка: {error}</div>}
{items.map(item => <Item key={item.id} item={item} />)}
{isLoading && <div>Загружаю...</div>}
</div>
);
}
Полный пример: правильная реализация
function InfiniteScroll() {
const [items, setItems] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const [hasMore, setHasMore] = useState(true);
const [page, setPage] = useState(1);
const isFetchingRef = useRef(false);
const bottomRef = useRef(null);
// Используй Intersection Observer
useEffect(() => {
const observer = new IntersectionObserver(
entries => {
if (entries[0].isIntersecting && hasMore && !isLoading && !isFetchingRef.current) {
fetchMore();
}
},
{ threshold: 0.1 }
);
if (bottomRef.current) observer.observe(bottomRef.current);
return () => {
if (bottomRef.current) observer.unobserve(bottomRef.current);
};
}, [hasMore, isLoading]);
const fetchMore = async () => {
if (isFetchingRef.current) return;
isFetchingRef.current = true;
setIsLoading(true);
setError(null);
try {
const response = await fetch(`/api/items?page=${page}`);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const { items: newItems, hasMore: more } = await response.json();
setItems(prev => [...prev, ...newItems]);
setHasMore(more);
setPage(prev => prev + 1);
} catch (err) {
setError(err.message);
} finally {
isFetchingRef.current = false;
setIsLoading(false);
}
};
return (
<div>
{error && <div className="error">Ошибка: {error}</div>}
{items.length === 0 && !isLoading && (
<div>Нет элементов</div>
)}
<div className="items-list">
{items.map(item => (
<ItemComponent key={item.id} item={item} />
))}
</div>
{isLoading && <div className="loader">Загружаю...</div>}
{!hasMore && <div>Больше нет элементов</div>}
{/* Сентинель для обнаружения конца */}
<div ref={bottomRef} style={{ height: '20px' }} />
</div>
);
}
Чеклист при реализации infinite scroll
[ ] Используешь Intersection Observer?
[ ] Правильно управляешь слушателями (cleanup)?
[ ] Предотвращаешь множественные запросы?
[ ] Обработал ошибки?
[ ] Показываешь loading состояние?
[ ] Виртуализировал список (для больших данных)?
[ ] Сохраняешь scroll position?
[ ] Проверил на дублирование элементов?
[ ] Показываешь "конец списка"?
[ ] Тестировал на медленной сети?
Итого
Основные проблемы infinite scroll:
- Memory leak - очищай слушателей
- Race conditions - проверяй isLoading
- Производительность - виртуализируй списки
- Обнаружение - используй IntersectionObserver
- Дублирование - отслеживай ID
- Scroll position - сохраняй и восстанавливай
- Error handling - обработай ошибки сети
- Edge cases - проверь конец списка, пустой список
Используй библиотеку react-infinite-scroll-component если нужно простое решение!