Skip to content

Commit 81415d5

Browse files
implement ScrollRestoration for home layout
1 parent 812426e commit 81415d5

File tree

2 files changed

+104
-63
lines changed

2 files changed

+104
-63
lines changed

packages/app/features/home/layout.web.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,21 @@ export function HomeLayout({
1414
children: React.ReactNode
1515
TopNav?: React.ReactNode
1616
} & ScrollViewProps & { fullHeight?: boolean }) {
17-
const { onScroll, onLayout, onContentSizeChange } = useScrollDirection()
17+
const { onScroll, onContentSizeChange, ref } = useScrollDirection()
18+
1819
return (
1920
<HomeSideBarWrapper>
2021
<BottomNavBarWrapper>
2122
<TagSearchProvider>
2223
<ScrollView
24+
ref={ref}
2325
mih="100%"
2426
contentContainerStyle={{
2527
mih: '100%',
2628
height: fullHeight ? '100%' : 'auto',
2729
}}
2830
scrollEventThrottle={128}
2931
onScroll={onScroll}
30-
onLayout={onLayout}
3132
onContentSizeChange={onContentSizeChange}
3233
showsVerticalScrollIndicator={false}
3334
{...props}

packages/app/provider/scroll/ScrollDirectionProvider.tsx

Lines changed: 101 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,21 @@
1-
import { usePathname } from 'app/utils/usePathname'
2-
import { createContext, useCallback, useContext, useEffect, useRef, useState } from 'react'
3-
import type { LayoutChangeEvent, NativeScrollEvent, NativeSyntheticEvent } from 'react-native'
1+
import { useRouter } from 'next/router'
2+
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
3+
import type {
4+
NativeScrollEvent,
5+
NativeSyntheticEvent,
6+
ScrollView,
7+
ScrollViewProps,
8+
} from 'react-native'
49
import { Dimensions } from 'react-native'
510

11+
export type ScrollPositions = Record<string, number>
12+
613
export type ScrollDirectionContextValue = {
714
direction: 'up' | 'down' | null
815
isAtEnd: boolean
9-
onScroll: (e: NativeSyntheticEvent<NativeScrollEvent>) => void
10-
onContentSizeChange: (width: number, height: number) => void
11-
onLayout: (e: LayoutChangeEvent) => void
16+
onScroll: ScrollViewProps['onScroll']
17+
onContentSizeChange: ScrollViewProps['onContentSizeChange']
18+
ref: React.RefObject<ScrollView>
1219
}
1320

1421
const ScrollDirection = createContext<ScrollDirectionContextValue>(
@@ -17,87 +24,120 @@ const ScrollDirection = createContext<ScrollDirectionContextValue>(
1724

1825
const THRESHOLD = 50
1926

27+
// Helper function to generate a unique key based only on pathname and query keys
28+
// @TODO naive approach, only seperates scroll positions by key
29+
const generateScrollKey = (
30+
pathname: string,
31+
query: Record<string, string | string[] | undefined>
32+
) => {
33+
// If no query, just return pathname
34+
if (Object.keys(query).length === 0) {
35+
return pathname
36+
}
37+
38+
// Sort query keys to create a consistent key
39+
const queryKeys = Object.keys(query).sort()
40+
41+
// Create a key with pathname and query keys in alphabetical order
42+
const queryString = queryKeys
43+
.map((key) => {
44+
// @TODO handle query params that should be unique
45+
if (key === 'token') {
46+
return `${key}=${query[key]}`
47+
}
48+
return key
49+
})
50+
.join('&')
51+
52+
return `${pathname}?${queryString}`
53+
}
54+
2055
export const ScrollDirectionProvider = ({ children }: { children: React.ReactNode }) => {
56+
const ref = useRef<ScrollView>(null)
57+
const { pathname, query } = useRouter()
58+
59+
// Get window dimensions
60+
const windowHeight = Dimensions.get('window').height
61+
62+
// Refs for performance-critical values
63+
const contentOffsetRef = useRef(0)
64+
65+
// State for UI updates
2166
const [direction, setDirection] = useState<ScrollDirectionContextValue['direction']>(null)
22-
const [isAtEnd, setIsAtEnd] = useState<ScrollDirectionContextValue['isAtEnd']>(false)
23-
const lastScrollY = useRef(0)
24-
const pathName = usePathname()
25-
const [, setPreviousPath] = useState('')
67+
const [contentHeight, setContentHeight] = useState<number>(0)
2668

27-
// Use window dimensions for initial values
28-
const initialHeight =
29-
typeof window !== 'undefined' ? window.innerHeight : Dimensions.get('window').height
30-
const [viewportHeight, setViewportHeight] = useState(initialHeight)
31-
const [contentHeight, setContentHeight] = useState(0)
69+
const [isAtEnd, setIsAtEnd] = useState(false)
70+
const [, setScrollPositions] = useState<ScrollPositions>({})
3271

3372
useEffect(() => {
34-
setPreviousPath((previousPath) => {
35-
if (previousPath !== pathName) {
36-
setDirection(null)
37-
setIsAtEnd(false)
38-
setContentHeight(0)
39-
setViewportHeight(initialHeight)
73+
const key = generateScrollKey(pathname, query)
74+
setScrollPositions((prev) => {
75+
if (prev[key] === undefined) {
76+
return { ...prev, [key]: 0 }
4077
}
41-
return pathName
78+
if (prev[key] !== undefined && contentHeight >= prev[key]) {
79+
ref.current?.scrollTo({ y: prev[key], animated: false })
80+
}
81+
return prev
4282
})
43-
}, [pathName, initialHeight])
44-
45-
const checkIsAtEnd = useCallback((contentHeight: number, viewportHeight: number, scrollY = 0) => {
46-
if (contentHeight === 0 || viewportHeight === 0) {
47-
return
83+
if (contentHeight <= windowHeight) {
84+
setDirection(null)
4885
}
86+
}, [pathname, query, contentHeight, windowHeight])
4987

50-
const isContentShorterThanViewport = contentHeight <= viewportHeight
51-
const isEndOfView =
52-
isContentShorterThanViewport || viewportHeight + scrollY >= contentHeight - THRESHOLD
53-
54-
setIsAtEnd(isEndOfView)
55-
}, [])
56-
88+
// Callback for content size change
5789
const onContentSizeChange = useCallback(
58-
(_: number, height: number) => {
90+
(w: number, height: number) => {
5991
setContentHeight(height)
60-
checkIsAtEnd(height, viewportHeight)
92+
setIsAtEnd(height <= windowHeight)
6193
},
62-
[viewportHeight, checkIsAtEnd]
63-
)
64-
65-
const onLayout = useCallback(
66-
(e: LayoutChangeEvent) => {
67-
const height = e.nativeEvent.layout.height
68-
setViewportHeight(height)
69-
checkIsAtEnd(contentHeight, height)
70-
},
71-
[contentHeight, checkIsAtEnd]
94+
[windowHeight]
7295
)
7396

97+
// Scroll event handler
7498
const onScroll = useCallback(
7599
(e: NativeSyntheticEvent<NativeScrollEvent>) => {
100+
const key = generateScrollKey(pathname, query)
76101
const { contentOffset } = e.nativeEvent
77-
const currentScrollY = contentOffset.y
78-
checkIsAtEnd(contentHeight, viewportHeight, currentScrollY)
102+
const contentOffsetY = contentOffset.y
79103

80-
// Update direction
81-
if (currentScrollY < THRESHOLD) {
104+
// Determine scroll direction
105+
if (contentOffsetY < THRESHOLD) {
82106
setDirection('up')
83-
} else if (lastScrollY.current - currentScrollY > THRESHOLD && !isAtEnd) {
107+
} else if (contentOffsetRef.current - contentOffsetY > THRESHOLD && !isAtEnd) {
84108
setDirection('up')
85-
} else if (currentScrollY - lastScrollY.current > THRESHOLD || isAtEnd) {
109+
} else if (contentOffsetY - contentOffsetRef.current > THRESHOLD || isAtEnd) {
86110
setDirection('down')
87111
}
88112

89-
lastScrollY.current = currentScrollY
113+
// Update last scroll position
114+
contentOffsetRef.current = contentOffsetY
115+
setScrollPositions((prev) => ({ ...prev, [key]: contentOffsetY }))
116+
117+
// Check if at the end of content
118+
const isScrollAtEnd =
119+
contentHeight > windowHeight
120+
? windowHeight + contentOffsetY >= contentHeight - THRESHOLD
121+
: false
122+
123+
setIsAtEnd(isScrollAtEnd)
90124
},
91-
[contentHeight, viewportHeight, isAtEnd, checkIsAtEnd]
125+
[isAtEnd, contentHeight, windowHeight, pathname, query]
92126
)
93-
94-
return (
95-
<ScrollDirection.Provider
96-
value={{ direction, isAtEnd, onScroll, onLayout, onContentSizeChange }}
97-
>
98-
{children}
99-
</ScrollDirection.Provider>
127+
// Memoized context value
128+
const value = useMemo(
129+
() => ({
130+
direction,
131+
isAtEnd,
132+
onScroll,
133+
onContentSizeChange,
134+
135+
ref,
136+
}),
137+
[direction, isAtEnd, onScroll, onContentSizeChange]
100138
)
139+
140+
return <ScrollDirection.Provider value={value}>{children}</ScrollDirection.Provider>
101141
}
102142

103143
export const useScrollDirection = () => {

0 commit comments

Comments
 (0)