Skip to content

Commit 6e9cbd4

Browse files
authored
Merge pull request #423 from AndreiCalazans/andrei/optimize-tab-view
refactor(container,lazy,hooks): avoid blocking JS thread by refactoring SharedValue.value access
2 parents 80dcd0c + 08a1c51 commit 6e9cbd4

16 files changed

+143
-179
lines changed

example/src/AnimatedHeader.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ export const Header = () => {
3030
{
3131
translateY: interpolate(
3232
top.value,
33-
[0, -(height.value || 0 - MIN_HEADER_HEIGHT)],
34-
[0, (height.value || 0 - MIN_HEADER_HEIGHT) / 2]
33+
[0, -(height || 0 - MIN_HEADER_HEIGHT)],
34+
[0, (height || 0 - MIN_HEADER_HEIGHT) / 2]
3535
),
3636
},
3737
],

example/src/FlashList.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ export const Header = () => {
3030
{
3131
translateY: interpolate(
3232
top.value,
33-
[0, -(height.value || 0 - MIN_HEADER_HEIGHT)],
34-
[0, (height.value || 0 - MIN_HEADER_HEIGHT) / 2]
33+
[0, -(height || 0 - MIN_HEADER_HEIGHT)],
34+
[0, (height || 0 - MIN_HEADER_HEIGHT) / 2]
3535
),
3636
},
3737
],

example/src/MasonryFlashList.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ export const Header = () => {
3030
{
3131
translateY: interpolate(
3232
top.value,
33-
[0, -(height.value || 0 - MIN_HEADER_HEIGHT)],
34-
[0, (height.value || 0 - MIN_HEADER_HEIGHT) / 2]
33+
[0, -(height || 0 - MIN_HEADER_HEIGHT)],
34+
[0, (height || 0 - MIN_HEADER_HEIGHT) / 2]
3535
),
3636
},
3737
],

example/src/Shared/Contacts.tsx

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -101,11 +101,7 @@ const renderItem = ({ item }: { item: Item }) => <ContactItem item={item} />
101101
const ListEmptyComponent = () => {
102102
const { top, height } = Tabs.useHeaderMeasurements()
103103
const translateY = useDerivedValue(() => {
104-
return interpolate(
105-
-top.value,
106-
[0, height.value || 0],
107-
[-(height.value || 0) / 2, 0]
108-
)
104+
return interpolate(-top.value, [0, height || 0], [-(height || 0) / 2, 0])
109105
}, [height])
110106

111107
const stylez = useAnimatedStyle(() => {

example/src/Shared/ContactsFlashList.tsx

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -101,11 +101,7 @@ const renderItem = ({ item }: { item: Item }) => <ContactItem item={item} />
101101
const ListEmptyComponent = () => {
102102
const { top, height } = Tabs.useHeaderMeasurements()
103103
const translateY = useDerivedValue(() => {
104-
return interpolate(
105-
-top.value,
106-
[0, height.value || 0],
107-
[-(height.value || 0) / 2, 0]
108-
)
104+
return interpolate(-top.value, [0, height || 0], [-(height || 0) / 2, 0])
109105
}, [height])
110106

111107
const stylez = useAnimatedStyle(() => {

example/src/Shared/ExampleMasonry.tsx

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -69,11 +69,7 @@ const ItemSeparator = () => <View style={styles.separator} />
6969
const ListEmptyComponent = () => {
7070
const { top, height } = Tabs.useHeaderMeasurements()
7171
const translateY = useDerivedValue(() => {
72-
return interpolate(
73-
-top.value,
74-
[0, height.value || 0],
75-
[-(height.value || 0) / 2, 0]
76-
)
72+
return interpolate(-top.value, [0, height || 0], [-(height || 0) / 2, 0])
7773
}, [height])
7874

7975
const stylez = useAnimatedStyle(() => {

example/src/Shared/SectionContacts.tsx

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -117,11 +117,7 @@ const renderItem = ({ item }: { item: Item }) => <ContactItem item={item} />
117117
const ListEmptyComponent = () => {
118118
const { top, height } = Tabs.useHeaderMeasurements()
119119
const translateY = useDerivedValue(() => {
120-
return interpolate(
121-
-top.value,
122-
[0, height.value || 0],
123-
[-(height.value || 0) / 2, 0]
124-
)
120+
return interpolate(-top.value, [0, height || 0], [-(height || 0) / 2, 0])
125121
}, [height])
126122

127123
const stylez = useAnimatedStyle(() => {

src/Container.tsx

Lines changed: 60 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,5 @@
11
import React from 'react'
2-
import {
3-
LayoutChangeEvent,
4-
StyleSheet,
5-
useWindowDimensions,
6-
View,
7-
} from 'react-native'
2+
import { StyleSheet, useWindowDimensions, View } from 'react-native'
83
import PagerView from 'react-native-pager-view'
94
import Animated, {
105
runOnJS,
@@ -15,6 +10,7 @@ import Animated, {
1510
useSharedValue,
1611
withDelay,
1712
withTiming,
13+
useFrameCallback,
1814
} from 'react-native-reanimated'
1915

2016
import { Context, TabNameContext } from './Context'
@@ -27,6 +23,7 @@ import {
2723
useContainerRef,
2824
usePageScrollHandler,
2925
useTabProps,
26+
useLayoutHeight,
3027
} from './hooks'
3128
import {
3229
CollapsibleProps,
@@ -93,25 +90,29 @@ export const Container = React.memo(
9390
const windowWidth = useWindowDimensions().width
9491
const width = customWidth ?? windowWidth
9592

96-
const containerHeight = useSharedValue<number | undefined>(undefined)
93+
const [containerHeight, getContainerLayoutHeight] = useLayoutHeight()
9794

98-
const tabBarHeight = useSharedValue<number | undefined>(
99-
initialTabBarHeight
100-
)
95+
const [tabBarHeight, getTabBarHeight] =
96+
useLayoutHeight(initialTabBarHeight)
10197

102-
const headerHeight = useSharedValue<number | undefined>(
98+
const [headerHeight, getHeaderHeight] = useLayoutHeight(
10399
!renderHeader ? 0 : initialHeaderHeight
104100
)
101+
const initialIndex = React.useMemo(
102+
() =>
103+
initialTabName
104+
? tabNamesArray.findIndex((n) => n === initialTabName)
105+
: 0,
106+
[initialTabName, tabNamesArray]
107+
)
105108

106-
const contentInset = useDerivedValue(() => {
109+
const contentInset = React.useMemo(() => {
107110
if (allowHeaderOverscroll) return 0
108111

109112
// necessary for the refresh control on iOS to be positioned underneath the header
110113
// this also adjusts the scroll bars to clamp underneath the header area
111-
return IS_IOS
112-
? (headerHeight.value || 0) + (tabBarHeight.value || 0)
113-
: 0
114-
})
114+
return IS_IOS ? (headerHeight || 0) + (tabBarHeight || 0) : 0
115+
}, [headerHeight, tabBarHeight, allowHeaderOverscroll])
115116

116117
const snappingTo: ContextType['snappingTo'] = useSharedValue(0)
117118
const offset: ContextType['offset'] = useSharedValue(0)
@@ -131,22 +132,16 @@ export const Container = React.memo(
131132
() => tabNamesArray,
132133
[tabNamesArray]
133134
)
134-
const index: ContextType['index'] = useSharedValue(
135-
initialTabName
136-
? tabNames.value.findIndex((n) => n === initialTabName)
137-
: 0
138-
)
135+
const index: ContextType['index'] = useSharedValue(initialIndex)
139136

140137
const focusedTab: ContextType['focusedTab'] =
141138
useDerivedValue<TabName>(() => {
142139
return tabNames.value[index.value]
143140
}, [tabNames])
144-
const calculateNextOffset = useSharedValue(index.value)
141+
const calculateNextOffset = useSharedValue(initialIndex)
145142
const headerScrollDistance: ContextType['headerScrollDistance'] =
146143
useDerivedValue(() => {
147-
return headerHeight.value !== undefined
148-
? headerHeight.value - minHeaderHeight
149-
: 0
144+
return headerHeight !== undefined ? headerHeight - minHeaderHeight : 0
150145
}, [headerHeight, minHeaderHeight])
151146

152147
const indexDecimal: ContextType['indexDecimal'] = useSharedValue(
@@ -167,7 +162,7 @@ export const Container = React.memo(
167162
scrollToImpl(
168163
refMap[name],
169164
0,
170-
scrollYCurrent.value - contentInset.value,
165+
scrollYCurrent.value - contentInset,
171166
false
172167
)
173168
}
@@ -213,6 +208,33 @@ export const Container = React.memo(
213208
[onIndexChange, onTabChange]
214209
)
215210

211+
const syncCurrentTabScrollPosition = () => {
212+
'worklet'
213+
214+
const name = tabNamesArray[index.value]
215+
scrollToImpl(
216+
refMap[name],
217+
0,
218+
scrollYCurrent.value - contentInset,
219+
false
220+
)
221+
}
222+
223+
/*
224+
* We run syncCurrentTabScrollPosition in every frame after the index
225+
* changes for about 1500ms because the Lists can be late to accept the
226+
* scrollTo event we send. This fixes the issue of the scroll position
227+
* jumping when the user changes tab.
228+
* */
229+
const toggleSyncScrollFrame = (toggle: boolean) =>
230+
syncScrollFrame.setActive(toggle)
231+
const syncScrollFrame = useFrameCallback(({ timeSinceFirstFrame }) => {
232+
syncCurrentTabScrollPosition()
233+
if (timeSinceFirstFrame > 1500) {
234+
runOnJS(toggleSyncScrollFrame)(false)
235+
}
236+
}, false)
237+
216238
useAnimatedReaction(
217239
() => {
218240
return calculateNextOffset.value
@@ -236,13 +258,14 @@ export const Container = React.memo(
236258
scrollYCurrent.value =
237259
scrollY.value[tabNames.value[index.value]] || 0
238260
}
261+
runOnJS(toggleSyncScrollFrame)(true)
239262
}
240263
},
241264
[]
242265
)
243266

244267
useAnimatedReaction(
245-
() => headerHeight.value,
268+
() => headerHeight,
246269
(_current, prev) => {
247270
if (prev === undefined) {
248271
// sync scroll if we started with undefined header height
@@ -267,32 +290,6 @@ export const Container = React.memo(
267290
}
268291
}, [revealHeaderOnScroll])
269292

270-
const getHeaderHeight = React.useCallback(
271-
(event: LayoutChangeEvent) => {
272-
const height = event.nativeEvent.layout.height
273-
if (headerHeight.value !== height) {
274-
headerHeight.value = height
275-
}
276-
},
277-
[headerHeight]
278-
)
279-
280-
const getTabBarHeight = React.useCallback(
281-
(event: LayoutChangeEvent) => {
282-
const height = event.nativeEvent.layout.height
283-
if (tabBarHeight.value !== height) tabBarHeight.value = height
284-
},
285-
[tabBarHeight]
286-
)
287-
288-
const onLayout = React.useCallback(
289-
(event: LayoutChangeEvent) => {
290-
const height = event.nativeEvent.layout.height
291-
if (containerHeight.value !== height) containerHeight.value = height
292-
},
293-
[containerHeight]
294-
)
295-
296293
const onTabPress = React.useCallback(
297294
(name: TabName) => {
298295
const i = tabNames.value.findIndex((n) => n === name)
@@ -302,7 +299,7 @@ export const Container = React.memo(
302299
runOnUI(scrollToImpl)(
303300
ref,
304301
0,
305-
headerScrollDistance.value - contentInset.value,
302+
headerScrollDistance.value - contentInset,
306303
true
307304
)
308305
} else {
@@ -313,11 +310,14 @@ export const Container = React.memo(
313310
[containerRef, refMap, contentInset]
314311
)
315312

316-
React.useEffect(() => {
317-
if (index.value >= tabNamesArray.length) {
318-
onTabPress(tabNamesArray[tabNamesArray.length - 1])
313+
useAnimatedReaction(
314+
() => tabNamesArray.length,
315+
(tabLength) => {
316+
if (index.value >= tabLength) {
317+
runOnJS(onTabPress)(tabNamesArray[tabLength - 1])
318+
}
319319
}
320-
}, [index.value, onTabPress, tabNamesArray])
320+
)
321321

322322
const pageScrollHandler = usePageScrollHandler({
323323
onPageScroll: (e) => {
@@ -381,7 +381,7 @@ export const Container = React.memo(
381381
>
382382
<Animated.View
383383
style={[styles.container, { width }, containerStyle]}
384-
onLayout={onLayout}
384+
onLayout={getContainerLayoutHeight}
385385
pointerEvents="box-none"
386386
>
387387
<Animated.View
@@ -430,7 +430,7 @@ export const Container = React.memo(
430430
<AnimatedPagerView
431431
ref={containerRef}
432432
onPageScroll={pageScrollHandler}
433-
initialPage={index.value}
433+
initialPage={initialIndex}
434434
{...pagerProps}
435435
style={[pagerProps?.style, StyleSheet.absoluteFill]}
436436
>

src/FlashList.tsx

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import Animated, {
1111
import {
1212
useChainCallback,
1313
useCollapsibleStyle,
14-
useConvertAnimatedToValue,
1514
useScrollHandlerY,
1615
useSharedAnimatedRef,
1716
useTabNameContext,
@@ -118,16 +117,14 @@ function FlashListImpl<R>(
118117
[progressViewOffset, refreshControl]
119118
)
120119

121-
const contentInsetValue = useConvertAnimatedToValue<number>(contentInset)
122-
123120
const memoContentInset = React.useMemo(
124-
() => ({ top: contentInsetValue }),
125-
[contentInsetValue]
121+
() => ({ top: contentInset }),
122+
[contentInset]
126123
)
127124

128125
const memoContentOffset = React.useMemo(
129-
() => ({ x: 0, y: -contentInsetValue }),
130-
[contentInsetValue]
126+
() => ({ x: 0, y: -contentInset }),
127+
[contentInset]
131128
)
132129

133130
const memoContentContainerStyle = React.useMemo(

src/FlatList.tsx

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import {
66
useAfterMountEffect,
77
useChainCallback,
88
useCollapsibleStyle,
9-
useConvertAnimatedToValue,
109
useScrollHandlerY,
1110
useSharedAnimatedRef,
1211
useTabNameContext,
@@ -79,16 +78,14 @@ function FlatListImpl<R>(
7978
[progressViewOffset, refreshControl]
8079
)
8180

82-
const contentInsetValue = useConvertAnimatedToValue(contentInset)
83-
8481
const memoContentInset = React.useMemo(
85-
() => ({ top: contentInsetValue }),
86-
[contentInsetValue]
82+
() => ({ top: contentInset }),
83+
[contentInset]
8784
)
8885

8986
const memoContentOffset = React.useMemo(
90-
() => ({ x: 0, y: -contentInsetValue }),
91-
[contentInsetValue]
87+
() => ({ x: 0, y: -contentInset }),
88+
[contentInset]
9289
)
9390

9491
const memoContentContainerStyle = React.useMemo(

0 commit comments

Comments
 (0)