Scroll Restoration in React Router
Source: Dev.to
Introduction
When building Single Page Applications (SPAs) with React Router, a common UX issue appears almost immediately: navigation changes the URL, but the page does not scroll to the top. This breaks user expectations, especially on content‑heavy pages like blogs, docs, or dashboards.
Traditional multi‑page websites vs. SPAs
| Traditional multi‑page sites | SPAs | |
|---|---|---|
| Navigation | Triggers a full page reload | Happens client‑side |
| DOM reload | Yes | No |
| Browser scroll reset | Automatically to (0, 0) | Preserved by default |
Consequently, navigating from /blog → /about leaves you scrolled halfway down the page.
Simple and correct implementation
import { useLayoutEffect } from "react";
import { useLocation } from "react-router-dom";
export function ScrollToTop() {
const { pathname } = useLocation();
useLayoutEffect(() => {
window.scrollTo(0, 0);
}, [pathname]);
return null;
}
useLocation()provides the current location object.pathnamechanges only when the route changes (not on query‑param or hash changes unless you include them).useLayoutEffectruns before the browser paints, preventing a visible jump/glitch.
Tip: Use
useLayoutEffectfor layout‑related side effects such as scroll, focus, or DOM measurement.
Where to mount the component
import { BrowserRouter } from "react-router-dom";
import { ScrollToTop } from "./ScrollToTop";
<BrowserRouter>
<ScrollToTop />
{/* ...your routes */}
</BrowserRouter>
- Mount it once at the root of your router.
- Do not place it inside individual pages or wrap routes individually.
Handling query parameters
If you want the scroll to reset when the query string changes:
import { useLayoutEffect } from "react";
import { useLocation } from "react-router-dom";
export function ScrollToTop() {
const { pathname, search } = useLocation();
useLayoutEffect(() => {
window.scrollTo(0, 0);
}, [pathname, search]);
return null;
}
Preserving anchor (hash) navigation
When the URL contains a hash (e.g., /docs#getting-started), the component above would override the browser’s default anchor scrolling. Fix it by ignoring hash changes:
import { useLayoutEffect } from "react";
import { useLocation } from "react-router-dom";
export function ScrollToTop() {
const { pathname, hash } = useLocation();
useLayoutEffect(() => {
if (hash) return; // let the browser handle anchor scrolling
window.scrollTo(0, 0);
}, [pathname, hash]);
return null;
}
Note: Browsers automatically remember scroll position on back/forward navigation. This component can interfere with that behavior, so it’s best used only for forward navigation or disabled on specific routes.
iOS and alternative scrolling methods
On some iOS devices, window.scrollTo may fail due to momentum scrolling. A safer fallback:
document.documentElement.scrollTop = 0;
document.body.scrollTop = 0;
Use the fallback only if you encounter issues.
Scrolling inside a custom container
If your app scrolls inside a specific element, target that element directly:
document.getElementById("scroll-root")?.scrollTo(0, 0);
Development quirks
- In development mode,
useLayoutEffectruns twice due to React’s strict mode. This does not affect production and can be ignored. - The effect runs only on navigation, performs a single synchronous operation, and causes zero re‑renders.
When not to use ScrollToTop
Preserving scroll position is preferable in certain UI patterns:
- Infinite‑scrolling pages
- Chat applications
- Forms with autosave
- Map‑based interfaces
In these cases, disabling automatic scroll‑to‑top improves the user experience.
Full example (including hash handling)
import { useLayoutEffect } from "react";
import { useLocation } from "react-router-dom";
export function ScrollToTop() {
const { pathname, hash } = useLocation();
useLayoutEffect(() => {
if (hash) return;
window.scrollTo({ top: 0, left: 0, behavior: "instant" });
}, [pathname, hash]);
return null;
}
Conclusion
Scroll restoration is not optional UI polish—it’s a core navigation expectation. This small component:
- Fixes a fundamental SPA flaw
- Improves perceived performance
- Makes your app feel “native”
If you’re using React Router and don’t have this behavior, users will notice—even if they don’t tell you.