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'
4
9
import { Dimensions } from 'react-native'
5
10
11
+ export type ScrollPositions = Record < string , number >
12
+
6
13
export type ScrollDirectionContextValue = {
7
14
direction : 'up' | 'down' | null
8
15
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 >
12
19
}
13
20
14
21
const ScrollDirection = createContext < ScrollDirectionContextValue > (
@@ -17,87 +24,120 @@ const ScrollDirection = createContext<ScrollDirectionContextValue>(
17
24
18
25
const THRESHOLD = 50
19
26
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
+
20
55
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
21
66
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 )
26
68
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 > ( { } )
32
71
33
72
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 }
40
77
}
41
- return pathName
78
+ if ( prev [ key ] !== undefined && contentHeight >= prev [ key ] ) {
79
+ ref . current ?. scrollTo ( { y : prev [ key ] , animated : false } )
80
+ }
81
+ return prev
42
82
} )
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 )
48
85
}
86
+ } , [ pathname , query , contentHeight , windowHeight ] )
49
87
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
57
89
const onContentSizeChange = useCallback (
58
- ( _ : number , height : number ) => {
90
+ ( w : number , height : number ) => {
59
91
setContentHeight ( height )
60
- checkIsAtEnd ( height , viewportHeight )
92
+ setIsAtEnd ( height <= windowHeight )
61
93
} ,
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 ]
72
95
)
73
96
97
+ // Scroll event handler
74
98
const onScroll = useCallback (
75
99
( e : NativeSyntheticEvent < NativeScrollEvent > ) => {
100
+ const key = generateScrollKey ( pathname , query )
76
101
const { contentOffset } = e . nativeEvent
77
- const currentScrollY = contentOffset . y
78
- checkIsAtEnd ( contentHeight , viewportHeight , currentScrollY )
102
+ const contentOffsetY = contentOffset . y
79
103
80
- // Update direction
81
- if ( currentScrollY < THRESHOLD ) {
104
+ // Determine scroll direction
105
+ if ( contentOffsetY < THRESHOLD ) {
82
106
setDirection ( 'up' )
83
- } else if ( lastScrollY . current - currentScrollY > THRESHOLD && ! isAtEnd ) {
107
+ } else if ( contentOffsetRef . current - contentOffsetY > THRESHOLD && ! isAtEnd ) {
84
108
setDirection ( 'up' )
85
- } else if ( currentScrollY - lastScrollY . current > THRESHOLD || isAtEnd ) {
109
+ } else if ( contentOffsetY - contentOffsetRef . current > THRESHOLD || isAtEnd ) {
86
110
setDirection ( 'down' )
87
111
}
88
112
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 )
90
124
} ,
91
- [ contentHeight , viewportHeight , isAtEnd , checkIsAtEnd ]
125
+ [ isAtEnd , contentHeight , windowHeight , pathname , query ]
92
126
)
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 ]
100
138
)
139
+
140
+ return < ScrollDirection . Provider value = { value } > { children } </ ScrollDirection . Provider >
101
141
}
102
142
103
143
export const useScrollDirection = ( ) => {
0 commit comments