При разработке одностраничных приложений на 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' });
Начиная с версии 6, React Router предоставляет встроенные хуки и
возможности для управления историей и состоянием. Для большинства
случаев достаточно сочетания useLocation и
useEffect. Более сложные сценарии требуют хранения позиций
скролла в объекте или контексте приложения.
setTimeout или ожидание завершения загрузки
данных.window.scrollTo и использовать альтернативы, если нужно
плавное восстановление.Автоматическое восстановление позиции скролла обеспечивает привычное и предсказуемое поведение навигации в одностраничных приложениях, улучшая пользовательский опыт и соответствуя стандартам веб-разработки.