Skip to content

Instantly share code, notes, and snippets.

@iDVB
Created August 27, 2024 15:07
Show Gist options
  • Save iDVB/4f6ba81d4cdf37dd16fefdfca0fd5777 to your computer and use it in GitHub Desktop.
Save iDVB/4f6ba81d4cdf37dd16fefdfca0fd5777 to your computer and use it in GitHub Desktop.
Custom NextJS ScrollRestoration with async triggerRestore
import * as React from 'react'
import { ScrollRestorationProvider } from './ScrollRestoration'
const ignorePattern = /^\/ai(\/.*)?$/
function MyApp(props) {
const { Component, pageProps, router } = props
return (
<ScrollRestorationProvider router={router} ignorePattern={ignorePattern}>
<Component {...pageProps} />
</ScrollRestorationProvider>
)
}
export default MyApp
// You can then optionally use 'ignorePattern' with ScrollRestorationProvider do turn off all scroll restoration
// for specific routes. You can then use useScrollRestorationContext to trigger "when" to restore scroll. This supports
// the scenerio where you want to setup Page Transitions where you want to only trigger the restore after/during the animation.
const { triggerRestore } = useScrollRestorationContext()
...
triggerRestore(route, false)
import React, { createContext, useContext, useEffect, useRef } from 'react'
import Head from 'next/head'
import Router from 'next/router'
const ScrollRestorationContext = createContext(null)
export const useScrollRestoration = ({ router, ignorePattern: optsIgnorePattern }) => {
const shouldScrollRestore = useRef(true)
const triggerRestore = React.useCallback(
(url, ignorePattern = optsIgnorePattern) => {
console.log({ scrollRestoration: window.history.scrollRestoration })
if (!ignorePattern?.test?.(url)) {
if (shouldScrollRestore.current) {
console.log('RESTORING')
setTimeout(() => {
shouldScrollRestore.current = false
restoreScrollPos(url)
}, 1)
} else {
console.log('SCROLLING TO TOP')
window.scrollTo(0, 0)
}
}
},
[optsIgnorePattern],
)
const restoreScrollPos = (url) => {
const json = sessionStorage.getItem(`scrollPos:${url}`)
const scrollPos = json ? JSON.parse(json) : undefined
if (scrollPos) {
window.scrollTo(scrollPos.x, scrollPos.y)
}
}
useEffect(() => {
if (typeof window === undefined || !('scrollRestoration' in window.history)) {
return
}
window.history.scrollRestoration = 'manual'
const saveScrollPos = (url) => {
sessionStorage.setItem(
`scrollPos:${url}`,
JSON.stringify({ x: window.scrollX, y: window.scrollY }),
)
}
const onBeforeUnload = (event) => {
saveScrollPos(router.asPath)
delete event['returnValue']
}
const onRouteChangeStart = () => {
saveScrollPos(router.asPath)
}
window.addEventListener('beforeunload', onBeforeUnload)
Router.events.on('routeChangeStart', onRouteChangeStart)
Router.beforePopState((state) => {
console.log('beforePopState')
state.options.scroll = false
shouldScrollRestore.current = true
return true
})
if (shouldScrollRestore.current) {
triggerRestore(router.asPath)
}
return () => {
window.removeEventListener('beforeunload', onBeforeUnload)
Router.events.off('routeChangeStart', onRouteChangeStart)
Router.beforePopState(() => true)
}
}, [router, triggerRestore])
return { triggerRestore }
}
export const useScrollRestorationContext = () => useContext(ScrollRestorationContext)
export const ScrollRestorationProvider = ({ children, router, ignorePattern }) => {
const { triggerRestore } = useScrollRestoration({ router, ignorePattern })
return (
<>
<Head>
<script
dangerouslySetInnerHTML={{
__html: `
window.history.scrollRestoration = 'manual'
`,
}}
/>
</Head>
<ScrollRestorationContext.Provider value={{ triggerRestore }}>
{children}
</ScrollRestorationContext.Provider>
</>
)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment