0%
Reading Settings
Font Size
18px
Line Height
1.5
Letter Spacing
0.01em
Font Family
Table of contents

How to restore scroll position in React Router 6
Frontend
Frontend

When developing a Single Page Application (SPA) with React and React Router V6, I encountered a scroll restoration issue. When I go to the new page, the scroll position persists instead of being on the top
React Router V6 offers a ScrollRestoration component, you can try it first! If you use V4 or V5, you can check this package: react-scroll-restoration. Because my application requirements are quite complicated so I decided to create a custom ScrollRestoration.
Problem
React Router behavior
The scroll position persists between pages when navigating using Link/navigate/browser back button... in the app.
Desired behavior
The scroll position persists between pages when navigating using Link/navigate/browser back button... in the app.
Desired behavior
- When I go to the new page, the scroll position should be on the top
- When users visit page A, scroll down, and click a link that takes them to page B. If they navigate back to page A using the browser's back button, the scroll position should be restored. If they navigate back to A using a link to A on page B, the scroll position should be at the top of the page.
Solution
To address this problem, we need a mechanism to store and restore the scroll position for each page in our application.
Instead of listening to popstate event (more details), I use key in the location object provided by React Router to detect if the browser back button clicked:
Instead of listening to popstate event (more details), I use key in the location object provided by React Router to detect if the browser back button clicked:
- When users click a link or we call navigate function, the key changes
- When users click a browser back button, the key does not change
Custom ScrollRestoration
// language: javascript import React, { useEffect, useState } from 'react' import { useBlocker, useLocation } from 'react-router-dom' type ScrollStates = { [key: string]: number } type PageKeys = { [key: string]: string } const ScrollRestoration = () => { const location = useLocation() const scrollableParent = document.querySelector('main') const [pageKeys, setPageKeys] = useState<PageKeys>({}) const [scrollStates, setScrollStates] = useState<ScrollStates>({}) useEffect(() => { if (!scrollableParent) return const pathName = location.pathname const clickedBackButton = pageKeys[pathName] === location.key if (clickedBackButton) { scrollableParent.scrollTo(0, scrollStates[pathName] || 0) } else { scrollableParent.scrollTo(0, 0) } const newPageKeys = { ...pageKeys, [pathName]: location.key } setPageKeys(newPageKeys) }, [location]) useBlocker(() => { if (scrollableParent) { const newScrollState = { ...scrollStates, [location.pathname]: scrollableParent.scrollTop } setScrollStates(newScrollState) } return false }) return <></> } export default ScrollRestoration
Code Explanation
- In most cases,
scrollableParent will be the window object. However, in my project, I specifically target the<main> tag for scrolling purposes. pageKeys is used to store the key associated with each pathname, whilescrollStates tracks the scroll position for each pathname. Because I add the ScrollRestoration component in the React Router layout, it doesn't remount during navigation. If you place elsewhere, wherepageKeys andscrollStates might be lost during navigation, consider using global state or sessionStorage instead of React state.clickedBackButton = pageKeys[pathName] === location.key determines whether the back button was clicked by comparing the current pathname's key with the location key (explained above)useBlocker is used to store the scroll position of the page before navigation (more details). It always returns false so as not to block navigation.
Happy coding!
Related blogs

Modern JavaScript OOP Features: True Private Fields, Static Methods, and More
When I first started learning JavaScript, its object-oriented features felt like an afterthought. The language had objects and prototypal inheritance, but it was missing key features like true private fields and methods. That’s why many developers tu...
Frontend
Frontend


JavaScript Immediately Invoked Function Expression
Today, we're diving into a common JavaScript pattern you've likely encountered: the Immediately Invoked Function Expression (IIFE). This pattern is used to manage scope and prevent global pollution.1. The ProblemConsider this simple script:// languag...
Frontend
Frontend
