Implement scroll restoration in the following conditions.
- There is no cache for fetched data(i.e., no library like
apollo
/react-query
) - There is no global state management, e.g.
redux
- No SSR. Just pure CSR.
- Use
react-router
for client-side routing
https://scroll-restoration.netlify.app/
- Scroll to a specific item on the current page
- Click a link to go to another page
- Repeat step 1., 2.
- Try browser's Back/Forward button. Scroll restoration should work.
1. Firebase
Firebase update the current page's state whenever an user scrolls. For example, if I scrollY
to 2226
on the https://firebase.google.com/
page, the console.log(history.state)
at that time will be
{
path: 'https://firebase.google.com/'
previousPath: undefined
scrollX: 0
scrollY: 2226 // the same value as the current `window.scrollY` value
}
I think on scroll
event they repeatly call history.replaceState({ ..updated state data.. }, '')
. And when an user navigate back, they do scroll restoration before adding scroll
event listener.
2. Wattenberger Blog -- It's a static website
Repo of the whole website.
I accidentally saw a working scroll restoration on this blog.
By the way, after reading their repo, I think their implementation works because there is not any async api call to fetch the content. It's a static website that their content already sit in the code. Therefore, the height of each page is always static.
-
[DONE] Make a custom hook and use functional component instead of class component
-
[DONE] Add async api call to make page's height dynamic
-
[DONE with pitfalls] Use
sessionStorage
to store scroll position of every page instead of the original implementation -
[DONE but create bugs as 6., 7.] Fix bug that occurs because of the execution order as described below (DOM updated happens before clearing the last effect):
render -> useEffect -> *re-render (update) -> *DOM updated -> *clear last effect -> useEffect render -> useEffect -> *render (mount) -> *clear last effect -> *DOM updated -> useEffect
-
[DONE] Use
window
'sscroll
event to save the scroll postion intosessionStorage
instead. -
Bug#1: Find '@BUG' keyword in the code to see more details
-
Bug#2: The
window
'sscroll
can be triggered by a browser when an user navigates to a new page which has different height from the previous page. Hence, the saved scroll position insessionStorage
can be replaced unexpectedly.
Because of bugs and many quirks, in the end, I decided to use implementation as explained in the Possible Solutions section instead.
- Modify
history
to save scroll position: https://github.com/janpaul123/delayed-scroll-restoration-polyfill - https://stackoverflow.com/a/52978973/6568503
- https://stackoverflow.com/a/58806940/6568503
- https://stackoverflow.com/a/57432069/6568503
- If an
<a>
tag was clicked, the returned function ofuseEffect()
will not be called, i.e.componentWillUnmount()
will not be called. - Because of 1., we cannot save the scroll position in
sessionStorage
or sethistory.scrollRestoration
back toauto
while an user navigating to an external link, i.e. clicking<a>
tag. - Setting
history.scrollRestoration = 'manual'
affects all websites in the current tab, not just our own website. - On Firefox, if we don't set
history.scrollRestoration = 'manual'
, the forward button will causewindow.scrollY = 0
before saving tosessionStorage
. - On Firefox, when we set
history.scrollRestoration = 'manual'
, we also have to manually callscrollTo(0)
on everyPUSH
page, not only onPOP
pages.