← Назад к вопросам

Как реализовывал таблицу с бесконечным 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 (эффективнее), виртуализацию для больших списков, кеширование, и обязательно обработку ошибок и дедупликацию данных.

Как реализовывал таблицу с бесконечным scroll? | PrepBro