-
-
Save claus/992a5596d6532ac91b24abe24e10ae81 to your computer and use it in GitHub Desktop.
import useScrollRestoration from "utils/hooks/useScrollRestoration"; | |
const App = ({ Component, pageProps, router }) => { | |
useScrollRestoration(router); | |
return <Component {...pageProps} />; | |
}; | |
export default App; |
import { useEffect } from 'react'; | |
import Router from 'next/router'; | |
function saveScrollPos(url) { | |
const scrollPos = { x: window.scrollX, y: window.scrollY }; | |
sessionStorage.setItem(url, JSON.stringify(scrollPos)); | |
} | |
function restoreScrollPos(url) { | |
const scrollPos = JSON.parse(sessionStorage.getItem(url)); | |
if (scrollPos) { | |
window.scrollTo(scrollPos.x, scrollPos.y); | |
} | |
} | |
export default function useScrollRestoration(router) { | |
useEffect(() => { | |
if ('scrollRestoration' in window.history) { | |
let shouldScrollRestore = false; | |
window.history.scrollRestoration = 'manual'; | |
restoreScrollPos(router.asPath); | |
const onBeforeUnload = event => { | |
saveScrollPos(router.asPath); | |
delete event['returnValue']; | |
}; | |
const onRouteChangeStart = () => { | |
saveScrollPos(router.asPath); | |
}; | |
const onRouteChangeComplete = url => { | |
if (shouldScrollRestore) { | |
shouldScrollRestore = false; | |
restoreScrollPos(url); | |
} | |
}; | |
window.addEventListener('beforeunload', onBeforeUnload); | |
Router.events.on('routeChangeStart', onRouteChangeStart); | |
Router.events.on('routeChangeComplete', onRouteChangeComplete); | |
Router.beforePopState(() => { | |
shouldScrollRestore = true; | |
return true; | |
}); | |
return () => { | |
window.removeEventListener('beforeunload', onBeforeUnload); | |
Router.events.off('routeChangeStart', onRouteChangeStart); | |
Router.events.off('routeChangeComplete', onRouteChangeComplete); | |
Router.beforePopState(() => true); | |
}; | |
} | |
}, [router]); | |
} |
I'm back again. I updated and simplified the above scripts pretty drastically. I'm also using window.history.state.key to track positions, so it supports going to the same page throughout history and always restoring the correct scroll position. This also fixes iOS Snapshots, since I don't set window.scrollRestoration to manual.
I also allow you to set a selector for an element to restore scroll to, for my website I have an "overflow-y-auto" child element that I need scrollRestoration for, so window won't do the trick. Here is my updated, versatile script for 2024 lol.
I also added an option to add a delay if you need to, and I moved router into the options, it will default to useRouter if you don't provide one.
useScrollRestoration.js
import { useRouter } from 'next/router'
import { useEffect, useState } from 'react'
export const saveScrollPos = (key, selector) => {
let scrollPos = { x: window.scrollX, y: window.scrollY }
if (selector) {
const element = document.querySelector(selector)
scrollPos = { x: element.scrollLeft, y: element.scrollTop }
}
sessionStorage.setItem(`scrollPos:${key}`, JSON.stringify(scrollPos))
}
export const restoreScrollPos = (key, selector) => {
const json = sessionStorage.getItem(`scrollPos:${key}`)
if (!json) return
const scrollPos = JSON.parse(json)
if (scrollPos?.x || scrollPos?.y) {
if (selector) {
const element = document.querySelector(selector)
element.scrollTo(scrollPos.x, scrollPos.y)
} else {
window.scrollTo(scrollPos.x, scrollPos.y)
}
}
}
export const deleteScrollPos = (key) => {
sessionStorage.removeItem(`scrollPos:${key}`)
}
export function useScrollRestoration({ enabled = true, router = null, selector = null, delay = null } = {}) {
const nextRouter = useRouter()
if (!router) {
router = nextRouter
}
const [key, setKey] = useState(null)
const [shouldRestore, setShouldRestore] = useState(false)
useEffect(() => {
setKey(window.history.state.key)
}, [])
useEffect(() => {
if (!enabled) return
const onBeforeUnload = () => {
deleteScrollPos(key)
deleteScrollPos(window.history.state.key)
}
const onRouteChangeStart = () => {
saveScrollPos(key, selector)
}
const onRouteChangeComplete = () => {
setKey(window.history.state.key)
if (!shouldRestore) return
setShouldRestore(false)
if (delay != null) {
setTimeout(() => {
restoreScrollPos(window.history.state.key, selector, delay)
deleteScrollPos(window.history.state.key)
}, delay)
} else {
restoreScrollPos(window.history.state.key, selector, delay)
deleteScrollPos(window.history.state.key)
}
}
window.addEventListener('beforeunload', onBeforeUnload)
router.events.on('routeChangeStart', onRouteChangeStart)
router.events.on('routeChangeComplete', onRouteChangeComplete)
router.beforePopState(() => {
setShouldRestore(true)
return true
})
return () => {
window.removeEventListener('beforeunload', onBeforeUnload)
router.events.off('routeChangeStart', onRouteChangeStart)
router.events.off('routeChangeComplete', onRouteChangeComplete)
router.beforePopState(() => true)
}
}, [enabled, key, selector, shouldRestore])
}
Default setup:
useScrollRestoration()
Scroll child element <main>
with overflow:
useScrollRestoration(router, { selector: "main" })
Delay Example:
useScrollRestoration(router, { delay: 100 })
Okay I've just got done doing tons of testing and the solution I've landed on is to use NextJS's experimental support.
However to fix iOS snapshots you must wrap your app with this:
This only works for pages that are rendered instantly. If you are loading data or the page isn't ready, than you'll need to use one of the solutions above. But make sure you modify it to set scrollRestoration to "auto" so iOS snapshots continue to function normally.