Автоматическое восстановление позиции скролла

При разработке одностраничных приложений на React с использованием React Router часто возникает необходимость управлять поведением прокрутки при переходе между страницами. В стандартной реализации браузера при переходе по ссылкам внутри приложения прокрутка может оставаться на прежнем месте, что нарушает привычное поведение веб-сайтов. Для решения этой задачи используется механизм автоматического восстановления позиции скролла.

Проблема стандартного поведения

По умолчанию, React Router не сбрасывает позицию скролла при переходе между маршрутами, так как он не вызывает полноценной перезагрузки страницы. Это означает, что если пользователь прокрутил страницу вниз на одной маршруте и перешёл на другой маршрут, позиция прокрутки останется на предыдущем месте. Такое поведение удобно при некоторых интерфейсных сценариях, но для большинства сайтов и приложений оно нарушает ожидаемый UX.

Базовый подход с useEffect

Самый простой способ контролировать позицию скролла — использовать хук useEffect в сочетании с хранилищем истории маршрутов. Например:

import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';

function ScrollToTop() {
  const { pathname } = useLocation();

  useEffect(() => {
    window.scrollTo(0, 0);
  }, [pathname]);

  return null;
}

export default ScrollToTop;

В этом примере компонент ScrollToTop отслеживает изменения пути с помощью useLocation и принудительно прокручивает страницу в верхнюю часть при каждом изменении маршрута.

Интеграция с <BrowserRouter>

Чтобы этот механизм работал на всём приложении, компонент ScrollToTop необходимо разместить внутри обёртки маршрутизатора, но выше компонентов маршрутов:

import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import ScrollToTop from './ScrollToTop';
import Home from './Home';
import About from './About';

function App() {
  return (
    <Router>
      <ScrollToTop />
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
      </Routes>
    </Router>
  );
}

Таким образом, каждый раз при переходе между Home и About позиция скролла будет сбрасываться в верхнюю точку.

Сохранение позиции при навигации «назад»

Для более сложных сценариев важно сохранять позицию прокрутки при использовании кнопок браузера «назад» и «вперед». Это достигается использованием API истории и состояния:

import { useEffect } from 'react';
import { useLocation, useNavigationType } from 'react-router-dom';

const scrollPositions = new Map();

function ScrollRestoration() {
  const location = useLocation();
  const navigationType = useNavigationType();

  useEffect(() => {
    if (navigationType === 'POP') {
      const savedPosition = scrollPositions.get(location.key);
      if (savedPosition) {
        window.scrollTo(savedPosition.x, savedPosition.y);
        return;
      }
    }
    window.scrollTo(0, 0);
  }, [location, navigationType]);

  useEffect(() => {
    return () => {
      scrollPositions.set(location.key, {
        x: window.scrollX,
        y: window.scrollY,
      });
    };
  }, [location]);

  return null;
}

export default ScrollRestoration;

В этом примере:

  • useNavigationType определяет тип навигации: переход по ссылке (PUSH) или возврат/перемещение в истории (POP).
  • Позиция прокрутки сохраняется при уходе с маршрута и восстанавливается при возвращении на него.
  • Использование location.key позволяет уникально идентифицировать каждый маршрут и хранить его позицию скролла.

Управление скроллом для определённых маршрутов

Иногда требуется сбрасывать скролл только для некоторых маршрутов или управлять прокруткой на конкретной оси. Можно добавить условие внутри эффекта:

useEffect(() => {
  if (location.pathname.startsWith('/articles')) {
    window.scrollTo(0, 0);
  }
}, [location]);

Также можно использовать плавную прокрутку с помощью beh * avior: 'smooth':

window.scrollTo({ top: 0, beh * avior: 'smooth' });

Автоматизация с React Router v6+

Начиная с версии 6, React Router предоставляет встроенные хуки и возможности для управления историей и состоянием. Для большинства случаев достаточно сочетания useLocation и useEffect. Более сложные сценарии требуют хранения позиций скролла в объекте или контексте приложения.

Важные нюансы

  • Необходимо учитывать асинхронную загрузку контента: если страница загружается динамически, скролл может сбрасываться до того, как контент будет виден, и пользователь увидит пустую страницу сверху. Решение — использовать setTimeout или ожидание завершения загрузки данных.
  • Для мобильных браузеров важно проверять совместимость с window.scrollTo и использовать альтернативы, если нужно плавное восстановление.

Автоматическое восстановление позиции скролла обеспечивает привычное и предсказуемое поведение навигации в одностраничных приложениях, улучшая пользовательский опыт и соответствуя стандартам веб-разработки.