Skip to content

Commit

Permalink
Restore scroll position on reload and back_forward
Browse files Browse the repository at this point in the history
  • Loading branch information
lubej committed Jun 16, 2023
1 parent 0db3f33 commit c3c76b6
Show file tree
Hide file tree
Showing 3 changed files with 132 additions and 6 deletions.
45 changes: 45 additions & 0 deletions src/app/hooks/useNavigationMetrics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { MutableRefObject, useLayoutEffect, useRef } from 'react'

export const useNavigationMetrics: () => MutableRefObject<{
reload: boolean
backForward: boolean
newPage: boolean
}> = () => {
const pageMetrics = useRef({
reload: false,
backForward: false,
newPage: false,
})

useLayoutEffect(() => {
const performanceObserver = new PerformanceObserver(observedEntries => {
const entryList: PerformanceNavigationTiming[] = observedEntries.getEntriesByType(
'navigation',
) as PerformanceNavigationTiming[]

entryList.forEach(entry => {
switch (entry.type) {
case 'reload': {
pageMetrics.current.reload = true
return
}
case 'back_forward': {
pageMetrics.current.backForward = true
return
}
default: {
pageMetrics.current.newPage = true
return
}
}
})
})

performanceObserver.observe({
type: 'navigation',
buffered: true,
})
}, [])

return pageMetrics
}
13 changes: 13 additions & 0 deletions src/app/hooks/usePageHide.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { useEffect } from 'react'

export const usePageHide = (
callback: (event: PageTransitionEvent) => void,
options?: EventListenerOptions,
) => {
useEffect(() => {
window.addEventListener('pagehide', callback, options)
return () => {
window.removeEventListener('pagehide', callback, options)
}
}, [callback, options])
}
80 changes: 74 additions & 6 deletions src/app/hooks/useScrollToTop.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,87 @@
import { useLocation } from 'react-router-dom'
import { useEffect } from 'react'
import { useLocation, useNavigation, UNSAFE_DataRouterContext as DataRouterContext } from 'react-router-dom'
import { useCallback, useContext, useEffect, useLayoutEffect, useRef } from 'react'
import { useNavigationMetrics } from './useNavigationMetrics'
import { usePageHide } from './usePageHide'

export const SCROLL_RESTORATION_KEY = 'scroll_restoration'

let savedScrollPositions: Record<string, number> = {}

export const useScrollToTop = ({ ignorePaths = [] }: { ignorePaths: RegExp[] }) => {
const dataRouterContext = useContext(DataRouterContext)
const navigation = useNavigation()
const canControlScrollRestoration = useRef(false)
const navigationMetrics = useNavigationMetrics()
const { reload, backForward, newPage } = navigationMetrics.current
const { pathname } = useLocation()

useEffect(() => {
const canControlScrollRestoration = 'scrollRestoration' in window.history
if (canControlScrollRestoration) {
const scrollRestoration = 'scrollRestoration' in window.history
if (scrollRestoration) {
window.history.scrollRestoration = 'manual'
canControlScrollRestoration.current = true
}

return () => {
window.history.scrollRestoration = 'auto'
}
}, [])

usePageHide(
useCallback(() => {
if (navigation.state === 'idle') {
savedScrollPositions[pathname] = window.scrollY
}
sessionStorage.setItem(SCROLL_RESTORATION_KEY, JSON.stringify(savedScrollPositions))
window.history.scrollRestoration = 'auto'
}, [navigation.state, pathname]),
)

useLayoutEffect(() => {
try {
const sessionPositions = sessionStorage.getItem(SCROLL_RESTORATION_KEY)
if (sessionPositions) {
savedScrollPositions = JSON.parse(sessionPositions)
}
} catch (e) {
// Ignore error
}
}, [])

// Enable scroll restoration in the router
useLayoutEffect(() => {
const disableScrollRestoration = dataRouterContext?.router?.enableScrollRestoration(
savedScrollPositions,
() => window.scrollY,
)
return () => disableScrollRestoration && disableScrollRestoration()
}, [dataRouterContext?.router])

useEffect(() => {
if (!canControlScrollRestoration.current) {
return
}

if (ignorePaths.some((path: RegExp) => path.test(pathname))) {
return
}

window.scrollTo(0, 0)
}, [ignorePaths, pathname])
const top = savedScrollPositions?.[pathname] ?? 0

if (reload || backForward) {
window.scrollTo({
top,
behavior: 'auto',
})
return
} else if (newPage) {
window.scrollTo({ top: 0, behavior: 'instant' })
}

navigationMetrics.current = {
backForward: false,
reload: false,
newPage: false,
}
}, [ignorePaths, pathname, navigationMetrics, backForward, newPage, reload])
}

0 comments on commit c3c76b6

Please sign in to comment.