← Назад к вопросам
Как реализовывал таблицу с бесконечным scroll?
1.0 Junior🔥 121 комментариев
#JavaScript Core
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI3 апр. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Бесконечный scroll: реализация и оптимизация
Бесконечный scroll (infinite scroll) — это техника загрузки данных по мере того, как пользователь скроллит вниз. Это популярно в социальных сетях, поисковых результатах, каталогах товаров.
Подход 1: Пересечение (Intersection Observer)
Используем Intersection Observer API для детектирования когда пользователь прокрутился к концу списка:
import { useEffect, useRef, useState } from 'react';
function InfiniteScrollTable() {
const [items, setItems] = useState([]);
const [page, setPage] = useState(1);
const [hasMore, setHasMore] = useState(true);
const [loading, setLoading] = useState(false);
const observerTarget = useRef(null);
// Загружаем данные
const loadMore = async () => {
if (loading || !hasMore) return;
setLoading(true);
try {
const response = await fetch(`/api/items?page=${page}&limit=20`);
const newItems = await response.json();
setItems(prev => [...prev, ...newItems.data]);
setPage(prev => prev + 1);
setHasMore(newItems.hasMore);
} catch (err) {
console.error('Error loading items:', err);
} finally {
setLoading(false);
}
};
// Intersection Observer
useEffect(() => {
const observer = new IntersectionObserver(
entries => {
// Если элемент в фокусе экрана
if (entries[0].isIntersecting && !loading && hasMore) {
loadMore();
}
},
{ threshold: 0.1 } // Триггер когда 10% видимо
);
if (observerTarget.current) {
observer.observe(observerTarget.current);
}
return () => observer.disconnect();
}, [loading, hasMore]);
return (
<div>
<table>
<tbody>
{items.map(item => (
<tr key={item.id}>
<td>{item.name}</td>
<td>{item.value}</td>
</tr>
))}
</tbody>
</table>
{/* Этот элемент триггеррит загрузку */}
<div ref={observerTarget} />
{loading && <p>Loading...</p>}
{!hasMore && <p>No more items</p>}
</div>
);
}
Подход 2: Window scroll event
Детектируем скролл окна (менее эффективно, но проще):
function InfiniteScrollTable() {
const [items, setItems] = useState([]);
const [page, setPage] = useState(1);
const [loading, setLoading] = useState(false);
const loadMore = async () => {
if (loading) return;
setLoading(true);
const response = await fetch(`/api/items?page=${page}`);
const newItems = await response.json();
setItems(prev => [...prev, ...newItems.data]);
setPage(prev => prev + 1);
setLoading(false);
};
useEffect(() => {
const handleScroll = () => {
// Проверяем если пользователь скроллил к концу
const scrollTop = window.scrollY;
const windowHeight = window.innerHeight;
const documentHeight = document.documentElement.scrollHeight;
// Если до конца осталось меньше 500px
if (scrollTop + windowHeight >= documentHeight - 500) {
loadMore();
}
};
// Используем throttle для производительности
let timeoutId;
const throttledScroll = () => {
clearTimeout(timeoutId);
timeoutId = setTimeout(handleScroll, 200);
};
window.addEventListener('scroll', throttledScroll);
return () => {
window.removeEventListener('scroll', throttledScroll);
clearTimeout(timeoutId);
};
}, [loading]);
return (
<table>
<tbody>
{items.map(item => (
<tr key={item.id}>
<td>{item.name}</td>
</tr>
))}
</tbody>
</table>
);
}
Проблема: Performance с большим количеством элементов
С тысячами элементов таблица будет тормозить. Решение: виртуализация (virtual scrolling):
import { FixedSizeList } from 'react-window';
function VirtualizedInfiniteScroll() {
const [items, setItems] = useState([]);
const [page, setPage] = useState(1);
const [hasMore, setHasMore] = useState(true);
const observerTarget = useRef(null);
const loadMore = async () => {
const response = await fetch(`/api/items?page=${page}`);
const newItems = await response.json();
setItems(prev => [...prev, ...newItems.data]);
setPage(prev => prev + 1);
setHasMore(newItems.hasMore);
};
useEffect(() => {
const observer = new IntersectionObserver(
entries => {
if (entries[0].isIntersecting && hasMore) {
loadMore();
}
},
{ threshold: 0.1 }
);
if (observerTarget.current) {
observer.observe(observerTarget.current);
}
return () => observer.disconnect();
}, [hasMore]);
const Row = ({ index, style }) => (
<div style={style}>
{items[index]?.name}
</div>
);
return (
<>
<FixedSizeList
height={600}
itemCount={items.length}
itemSize={35}
width="100%"
>
{Row}
</FixedSizeList>
<div ref={observerTarget} />
</>
);
}
Оптимизация: Дедупликация и кеширование
function InfiniteScrollWithCache() {
const [items, setItems] = useState([]);
const [page, setPage] = useState(1);
const cacheRef = useRef(new Map());
const loadMore = async () => {
// Проверяем кеш
if (cacheRef.current.has(page)) {
setItems(prev => [...prev, ...cacheRef.current.get(page)]);
setPage(prev => prev + 1);
return;
}
const response = await fetch(`/api/items?page=${page}`);
const newItems = await response.json();
// Дедупликация
const uniqueItems = newItems.data.filter(
newItem => !items.some(item => item.id === newItem.id)
);
// Кешируем
cacheRef.current.set(page, uniqueItems);
setItems(prev => [...prev, ...uniqueItems]);
setPage(prev => prev + 1);
};
// ...
}
Best Practice
import { useCallback, useEffect, useRef, useState } from 'react';
function OptimizedInfiniteScroll() {
const [items, setItems] = useState([]);
const [page, setPage] = useState(1);
const [hasMore, setHasMore] = useState(true);
const [loading, setLoading] = useState(false);
const observerTarget = useRef(null);
const loadingRef = useRef(false);
const loadMore = useCallback(async () => {
if (loadingRef.current || !hasMore) return;
loadingRef.current = true;
setLoading(true);
try {
const response = await fetch(
`/api/items?page=${page}&limit=20`,
{ signal: AbortSignal.timeout(5000) } // Таймаут 5с
);
if (!response.ok) throw new Error('Failed to load');
const { data, nextPage, hasMore: more } = await response.json();
setItems(prev => [
...prev,
...data.filter(item => !prev.some(p => p.id === item.id))
]);
setPage(nextPage);
setHasMore(more);
} catch (error) {
console.error('Error loading items:', error);
// Показать ошибку пользователю
} finally {
loadingRef.current = false;
setLoading(false);
}
}, [page, hasMore]);
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
loadMore();
}
},
{ threshold: 0.1, rootMargin: '50px' }
);
if (observerTarget.current) {
observer.observe(observerTarget.current);
}
return () => observer.disconnect();
}, [loadMore]);
return (
<>
<table>
<tbody>
{items.map(item => (
<tr key={item.id}>
<td>{item.name}</td>
</tr>
))}
</tbody>
</table>
<div ref={observerTarget} className="loader" />
{loading && <div>Loading...</div>}
</>
);
}
Вывод: для бесконечного scroll используй Intersection Observer (эффективнее), виртуализацию для больших списков, кеширование, и обязательно обработку ошибок и дедупликацию данных.