Сохранение позиции скролла

В библиотеке React Aria управление скроллом реализуется через хуки и утилиты, обеспечивающие доступность и предсказуемое поведение в интерфейсе. Основная задача — сохранять и восстанавливать позицию прокрутки при переходах между компонентами, обновлении контента или изменении состояния страницы.

React Aria не навязывает конкретную реализацию скролла, а предоставляет инструменты для отслеживания и управления состоянием прокрутки через refs, события и хуки.


Использование useScroll для отслеживания позиции

Хук useScroll позволяет привязать обработчики к элементу с прокруткой и отслеживать текущую позицию:

import {useScroll} from '@react-aria/interactions';
import {useRef} from 'react';

function ScrollableList() {
  const scrollRef = useRef(null);
  const {scrollProps} = useScroll({
    onScroll: (e) => {
      console.log('Текущая позиция scrollTop:', e.target.scrollTop);
    }
  });

  return (
    <div {...scrollProps} ref={scrollRef} style={{overflowY: 'auto', height: 300}}>
      {/* Контент */}
    </div>
  );
}

Ключевые моменты:

  • scrollRef используется для прямого доступа к DOM-элементу.
  • scrollProps включает стандартные события onScroll и другие атрибуты, обеспечивающие корректное поведение для ассистивных технологий.
  • onScroll срабатывает при любом изменении позиции прокрутки.

Сохранение позиции скролла при навигации

Чтобы сохранять позицию при смене страниц или компонент, можно хранить координаты скролла в состоянии или контексте:

import {useState, useEffect, useRef} from 'react';

function ScrollManager({children}) {
  const [scrollPositions, setScrollPositions] = useState({});
  const scrollRef = useRef(null);

  useEffect(() => {
    const handleScroll = () => {
      setScrollPositions(prev => ({
        ...prev,
        main: scrollRef.current.scrollTop
      }));
    };

    const node = scrollRef.current;
    node.addEventListener('scroll', handleScroll);
    return () => node.removeEventListener('scroll', handleScroll);
  }, []);

  useEffect(() => {
    if (scrollRef.current && scrollPositions.main !== undefined) {
      scrollRef.current.scrollTop = scrollPositions.main;
    }
  }, [scrollPositions]);

  return (
    <div ref={scrollRef} style={{overflowY: 'auto', height: '100%'}}>
      {children}
    </div>
  );
}

Особенности реализации:

  • Позиция скролла хранится в объекте scrollPositions по ключам, соответствующим различным областям интерфейса.
  • При повторном рендере элемента scrollTop восстанавливается из состояния.
  • Такой подход особенно полезен при динамическом обновлении контента или при работе с роутингом.

Сложные сценарии: динамический контент

При асинхронной подгрузке элементов высота контейнера может изменяться, что сдвигает позицию скролла. Для корректного восстановления используется requestAnimationFrame или setTimeout:

useEffect(() => {
  if (scrollRef.current && scrollPositions.main !== undefined) {
    requestAnimationFrame(() => {
      scrollRef.current.scrollTop = scrollPositions.main;
    });
  }
}, [scrollPositions, children]);

Пояснение:

  • requestAnimationFrame откладывает установку позиции до следующего рендера, когда DOM уже полностью обновлен.
  • Гарантирует точное восстановление даже при изменении размеров элементов.

Использование useVirtualizer для больших списков

React Aria совместим с библиотекой React Stately и useVirtualizer, позволяя эффективно работать с длинными списками без потери позиции скролла:

import {useVirtualizer} from '@tanstack/react-virtual';

function VirtualList({items}) {
  const parentRef = useRef(null);
  const rowVirtualizer = useVirtualizer({
    count: items.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 50
  });

  return (
    <div ref={parentRef} style={{height: 400, overflowY: 'auto'}}>
      <div style={{height: `${rowVirtualizer.getTotalSize()}px`, position: 'relative'}}>
        {rowVirtualizer.getVirtualItems().map(virtualRow => (
          <div key={virtualRow.index}
               style={{
                 position: 'absolute',
                 top: 0,
                 left: 0,
                 width: '100%',
                 transform: `translateY(${virtualRow.start}px)`
               }}>
            {items[virtualRow.index]}
          </div>
        ))}
      </div>
    </div>
  );
}

Преимущества:

  • Поддержка тысяч элементов без падения производительности.
  • Позиция скролла автоматически синхронизируется с виртуальными элементами.
  • Интеграция с React Aria позволяет применять доступные атрибуты для каждого элемента списка.

Адаптивное управление фокусом и скроллом

При восстановлении скролла важно учитывать фокус. React Aria предоставляет хуки для синхронизации фокуса с прокруткой, например useFocusRing и useFocusManager:

import {useFocusManager} from '@react-aria/focus';

function FocusScroll() {
  const focusManager = useFocusManager();
  
  const scrollToFocused = () => {
    const focused = document.activeElement;
    if (focused && focused.scrollIntoView) {
      focused.scrollIntoView({beh * avior: 'smooth', block: 'nearest'});
    }
  };

  return (
    <button onCl ick={() => {
      focusManager.focusNext();
      scrollToFocused();
    }}>
      Следующий элемент
    </button>
  );
}

Ключевой момент:

  • Совмещение управления фокусом с прокруткой обеспечивает доступность интерфейса.
  • Метод scrollIntoView корректно работает с динамическим контентом и виртуализированными списками.

Практические рекомендации

  • Всегда использовать ref для контейнеров скролла, чтобы напрямую управлять scrollTop.
  • Для асинхронного контента применять отложенное восстановление позиции.
  • В больших списках использовать виртуализацию с интеграцией React Aria.
  • Сохранять позиции скролла по ключам, чтобы управлять несколькими областями интерфейса.
  • Комбинировать восстановление позиции с управлением фокусом для соблюдения принципов доступности.