Skip to content

Commit

Permalink
refactor(hooks, container): avoid reading .value outside worklet
Browse files Browse the repository at this point in the history
This is a performance optimization to avoid blocking the JavaScript
thread.

BREAKING CHANGE: headerHeight, tabBarHeight, containerHeight, and
contentInset are no longer SharedValues.

If you consume useHeaderMeasurements and/or useTabsContext expect this
to impact you.
  • Loading branch information
AndreiCalazans committed Jul 5, 2024
1 parent 80dcd0c commit 08a1c51
Show file tree
Hide file tree
Showing 16 changed files with 143 additions and 179 deletions.
4 changes: 2 additions & 2 deletions example/src/AnimatedHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ export const Header = () => {
{
translateY: interpolate(
top.value,
[0, -(height.value || 0 - MIN_HEADER_HEIGHT)],
[0, (height.value || 0 - MIN_HEADER_HEIGHT) / 2]
[0, -(height || 0 - MIN_HEADER_HEIGHT)],
[0, (height || 0 - MIN_HEADER_HEIGHT) / 2]
),
},
],
Expand Down
4 changes: 2 additions & 2 deletions example/src/FlashList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ export const Header = () => {
{
translateY: interpolate(
top.value,
[0, -(height.value || 0 - MIN_HEADER_HEIGHT)],
[0, (height.value || 0 - MIN_HEADER_HEIGHT) / 2]
[0, -(height || 0 - MIN_HEADER_HEIGHT)],
[0, (height || 0 - MIN_HEADER_HEIGHT) / 2]
),
},
],
Expand Down
4 changes: 2 additions & 2 deletions example/src/MasonryFlashList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ export const Header = () => {
{
translateY: interpolate(
top.value,
[0, -(height.value || 0 - MIN_HEADER_HEIGHT)],
[0, (height.value || 0 - MIN_HEADER_HEIGHT) / 2]
[0, -(height || 0 - MIN_HEADER_HEIGHT)],
[0, (height || 0 - MIN_HEADER_HEIGHT) / 2]
),
},
],
Expand Down
6 changes: 1 addition & 5 deletions example/src/Shared/Contacts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,11 +101,7 @@ const renderItem = ({ item }: { item: Item }) => <ContactItem item={item} />
const ListEmptyComponent = () => {
const { top, height } = Tabs.useHeaderMeasurements()
const translateY = useDerivedValue(() => {
return interpolate(
-top.value,
[0, height.value || 0],
[-(height.value || 0) / 2, 0]
)
return interpolate(-top.value, [0, height || 0], [-(height || 0) / 2, 0])
}, [height])

const stylez = useAnimatedStyle(() => {
Expand Down
6 changes: 1 addition & 5 deletions example/src/Shared/ContactsFlashList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,11 +101,7 @@ const renderItem = ({ item }: { item: Item }) => <ContactItem item={item} />
const ListEmptyComponent = () => {
const { top, height } = Tabs.useHeaderMeasurements()
const translateY = useDerivedValue(() => {
return interpolate(
-top.value,
[0, height.value || 0],
[-(height.value || 0) / 2, 0]
)
return interpolate(-top.value, [0, height || 0], [-(height || 0) / 2, 0])
}, [height])

const stylez = useAnimatedStyle(() => {
Expand Down
6 changes: 1 addition & 5 deletions example/src/Shared/ExampleMasonry.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,7 @@ const ItemSeparator = () => <View style={styles.separator} />
const ListEmptyComponent = () => {
const { top, height } = Tabs.useHeaderMeasurements()
const translateY = useDerivedValue(() => {
return interpolate(
-top.value,
[0, height.value || 0],
[-(height.value || 0) / 2, 0]
)
return interpolate(-top.value, [0, height || 0], [-(height || 0) / 2, 0])
}, [height])

const stylez = useAnimatedStyle(() => {
Expand Down
6 changes: 1 addition & 5 deletions example/src/Shared/SectionContacts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -117,11 +117,7 @@ const renderItem = ({ item }: { item: Item }) => <ContactItem item={item} />
const ListEmptyComponent = () => {
const { top, height } = Tabs.useHeaderMeasurements()
const translateY = useDerivedValue(() => {
return interpolate(
-top.value,
[0, height.value || 0],
[-(height.value || 0) / 2, 0]
)
return interpolate(-top.value, [0, height || 0], [-(height || 0) / 2, 0])
}, [height])

const stylez = useAnimatedStyle(() => {
Expand Down
120 changes: 60 additions & 60 deletions src/Container.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
import React from 'react'
import {
LayoutChangeEvent,
StyleSheet,
useWindowDimensions,
View,
} from 'react-native'
import { StyleSheet, useWindowDimensions, View } from 'react-native'
import PagerView from 'react-native-pager-view'
import Animated, {
runOnJS,
Expand All @@ -15,6 +10,7 @@ import Animated, {
useSharedValue,
withDelay,
withTiming,
useFrameCallback,
} from 'react-native-reanimated'

import { Context, TabNameContext } from './Context'
Expand All @@ -27,6 +23,7 @@ import {
useContainerRef,
usePageScrollHandler,
useTabProps,
useLayoutHeight,
} from './hooks'
import {
CollapsibleProps,
Expand Down Expand Up @@ -93,25 +90,29 @@ export const Container = React.memo(
const windowWidth = useWindowDimensions().width
const width = customWidth ?? windowWidth

const containerHeight = useSharedValue<number | undefined>(undefined)
const [containerHeight, getContainerLayoutHeight] = useLayoutHeight()

const tabBarHeight = useSharedValue<number | undefined>(
initialTabBarHeight
)
const [tabBarHeight, getTabBarHeight] =
useLayoutHeight(initialTabBarHeight)

const headerHeight = useSharedValue<number | undefined>(
const [headerHeight, getHeaderHeight] = useLayoutHeight(
!renderHeader ? 0 : initialHeaderHeight
)
const initialIndex = React.useMemo(
() =>
initialTabName
? tabNamesArray.findIndex((n) => n === initialTabName)
: 0,
[initialTabName, tabNamesArray]
)

const contentInset = useDerivedValue(() => {
const contentInset = React.useMemo(() => {
if (allowHeaderOverscroll) return 0

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

const snappingTo: ContextType['snappingTo'] = useSharedValue(0)
const offset: ContextType['offset'] = useSharedValue(0)
Expand All @@ -131,22 +132,16 @@ export const Container = React.memo(
() => tabNamesArray,
[tabNamesArray]
)
const index: ContextType['index'] = useSharedValue(
initialTabName
? tabNames.value.findIndex((n) => n === initialTabName)
: 0
)
const index: ContextType['index'] = useSharedValue(initialIndex)

const focusedTab: ContextType['focusedTab'] =
useDerivedValue<TabName>(() => {
return tabNames.value[index.value]
}, [tabNames])
const calculateNextOffset = useSharedValue(index.value)
const calculateNextOffset = useSharedValue(initialIndex)
const headerScrollDistance: ContextType['headerScrollDistance'] =
useDerivedValue(() => {
return headerHeight.value !== undefined
? headerHeight.value - minHeaderHeight
: 0
return headerHeight !== undefined ? headerHeight - minHeaderHeight : 0
}, [headerHeight, minHeaderHeight])

const indexDecimal: ContextType['indexDecimal'] = useSharedValue(
Expand All @@ -167,7 +162,7 @@ export const Container = React.memo(
scrollToImpl(
refMap[name],
0,
scrollYCurrent.value - contentInset.value,
scrollYCurrent.value - contentInset,
false
)
}
Expand Down Expand Up @@ -213,6 +208,33 @@ export const Container = React.memo(
[onIndexChange, onTabChange]
)

const syncCurrentTabScrollPosition = () => {
'worklet'

const name = tabNamesArray[index.value]
scrollToImpl(
refMap[name],
0,
scrollYCurrent.value - contentInset,
false
)
}

/*
* We run syncCurrentTabScrollPosition in every frame after the index
* changes for about 1500ms because the Lists can be late to accept the
* scrollTo event we send. This fixes the issue of the scroll position
* jumping when the user changes tab.
* */
const toggleSyncScrollFrame = (toggle: boolean) =>
syncScrollFrame.setActive(toggle)
const syncScrollFrame = useFrameCallback(({ timeSinceFirstFrame }) => {
syncCurrentTabScrollPosition()
if (timeSinceFirstFrame > 1500) {
runOnJS(toggleSyncScrollFrame)(false)
}
}, false)

useAnimatedReaction(
() => {
return calculateNextOffset.value
Expand All @@ -236,13 +258,14 @@ export const Container = React.memo(
scrollYCurrent.value =
scrollY.value[tabNames.value[index.value]] || 0
}
runOnJS(toggleSyncScrollFrame)(true)
}
},
[]
)

useAnimatedReaction(
() => headerHeight.value,
() => headerHeight,
(_current, prev) => {
if (prev === undefined) {
// sync scroll if we started with undefined header height
Expand All @@ -267,32 +290,6 @@ export const Container = React.memo(
}
}, [revealHeaderOnScroll])

const getHeaderHeight = React.useCallback(
(event: LayoutChangeEvent) => {
const height = event.nativeEvent.layout.height
if (headerHeight.value !== height) {
headerHeight.value = height
}
},
[headerHeight]
)

const getTabBarHeight = React.useCallback(
(event: LayoutChangeEvent) => {
const height = event.nativeEvent.layout.height
if (tabBarHeight.value !== height) tabBarHeight.value = height
},
[tabBarHeight]
)

const onLayout = React.useCallback(
(event: LayoutChangeEvent) => {
const height = event.nativeEvent.layout.height
if (containerHeight.value !== height) containerHeight.value = height
},
[containerHeight]
)

const onTabPress = React.useCallback(
(name: TabName) => {
const i = tabNames.value.findIndex((n) => n === name)
Expand All @@ -302,7 +299,7 @@ export const Container = React.memo(
runOnUI(scrollToImpl)(
ref,
0,
headerScrollDistance.value - contentInset.value,
headerScrollDistance.value - contentInset,
true
)
} else {
Expand All @@ -313,11 +310,14 @@ export const Container = React.memo(
[containerRef, refMap, contentInset]
)

React.useEffect(() => {
if (index.value >= tabNamesArray.length) {
onTabPress(tabNamesArray[tabNamesArray.length - 1])
useAnimatedReaction(
() => tabNamesArray.length,
(tabLength) => {
if (index.value >= tabLength) {
runOnJS(onTabPress)(tabNamesArray[tabLength - 1])
}
}
}, [index.value, onTabPress, tabNamesArray])
)

const pageScrollHandler = usePageScrollHandler({
onPageScroll: (e) => {
Expand Down Expand Up @@ -381,7 +381,7 @@ export const Container = React.memo(
>
<Animated.View
style={[styles.container, { width }, containerStyle]}
onLayout={onLayout}
onLayout={getContainerLayoutHeight}
pointerEvents="box-none"
>
<Animated.View
Expand Down Expand Up @@ -430,7 +430,7 @@ export const Container = React.memo(
<AnimatedPagerView
ref={containerRef}
onPageScroll={pageScrollHandler}
initialPage={index.value}
initialPage={initialIndex}
{...pagerProps}
style={[pagerProps?.style, StyleSheet.absoluteFill]}
>
Expand Down
11 changes: 4 additions & 7 deletions src/FlashList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import Animated, {
import {
useChainCallback,
useCollapsibleStyle,
useConvertAnimatedToValue,
useScrollHandlerY,
useSharedAnimatedRef,
useTabNameContext,
Expand Down Expand Up @@ -118,16 +117,14 @@ function FlashListImpl<R>(
[progressViewOffset, refreshControl]
)

const contentInsetValue = useConvertAnimatedToValue<number>(contentInset)

const memoContentInset = React.useMemo(
() => ({ top: contentInsetValue }),
[contentInsetValue]
() => ({ top: contentInset }),
[contentInset]
)

const memoContentOffset = React.useMemo(
() => ({ x: 0, y: -contentInsetValue }),
[contentInsetValue]
() => ({ x: 0, y: -contentInset }),
[contentInset]
)

const memoContentContainerStyle = React.useMemo(
Expand Down
11 changes: 4 additions & 7 deletions src/FlatList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {
useAfterMountEffect,
useChainCallback,
useCollapsibleStyle,
useConvertAnimatedToValue,
useScrollHandlerY,
useSharedAnimatedRef,
useTabNameContext,
Expand Down Expand Up @@ -79,16 +78,14 @@ function FlatListImpl<R>(
[progressViewOffset, refreshControl]
)

const contentInsetValue = useConvertAnimatedToValue(contentInset)

const memoContentInset = React.useMemo(
() => ({ top: contentInsetValue }),
[contentInsetValue]
() => ({ top: contentInset }),
[contentInset]
)

const memoContentOffset = React.useMemo(
() => ({ x: 0, y: -contentInsetValue }),
[contentInsetValue]
() => ({ x: 0, y: -contentInset }),
[contentInset]
)

const memoContentContainerStyle = React.useMemo(
Expand Down
Loading

0 comments on commit 08a1c51

Please sign in to comment.