about summary refs log tree commit diff
path: root/src/lib/hooks/useWebScrollRestoration.ts
blob: 36484ba0f1dfbad4e0b7dbefab48296d166aa7b7 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import {useEffect, useMemo, useState} from 'react'
import {EventArg, useNavigation} from '@react-navigation/core'

if ('scrollRestoration' in history) {
  // Tell the brower not to mess with the scroll.
  // We're doing that manually below.
  history.scrollRestoration = 'manual'
}

function createInitialScrollState() {
  return {
    scrollYs: new Map(),
    focusedKey: null as string | null,
  }
}

export function useWebScrollRestoration() {
  const [state] = useState(createInitialScrollState)
  const navigation = useNavigation()

  useEffect(() => {
    function onDispatch() {
      if (state.focusedKey) {
        // Remember where we were for later.
        state.scrollYs.set(state.focusedKey, window.scrollY)
        // TODO: Strictly speaking, this is a leak. We never clean up.
        // This is because I'm not sure when it's appropriate to clean it up.
        // It doesn't seem like popstate is enough because it can still Forward-Back again.
        // Maybe we should use sessionStorage. Or check what Next.js is doing?
      }
    }
    // We want to intercept any push/pop/replace *before* the re-render.
    // There is no official way to do this yet, but this works okay for now.
    // https://twitter.com/satya164/status/1737301243519725803
    navigation.addListener('__unsafe_action__' as any, onDispatch)
    return () => {
      navigation.removeListener('__unsafe_action__' as any, onDispatch)
    }
  }, [state, navigation])

  const screenListeners = useMemo(
    () => ({
      focus(e: EventArg<'focus', boolean | undefined, unknown>) {
        const scrollY = state.scrollYs.get(e.target) ?? 0
        window.scrollTo(0, scrollY)
        state.focusedKey = e.target ?? null
      },
    }),
    [state],
  )
  return screenListeners
}