Skip to content

Commit

Permalink
fix: scroll restoration when using Lazy
Browse files Browse the repository at this point in the history
  • Loading branch information
andreialecu committed Apr 27, 2023
1 parent e93b45c commit 7c0ec48
Show file tree
Hide file tree
Showing 9 changed files with 165 additions and 79 deletions.
4 changes: 3 additions & 1 deletion example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import DynamicTabs from './DynamicTabs'
import FlashList from './FlashList'
import HeaderOverscrollExample from './HeaderOverscroll'
import Lazy from './Lazy'
import LazyNoFade from './LazyNoFade'
import MinHeaderHeight from './MinHeaderHeight'
import OnTabChange from './OnTabChange'
import QuickStartDemo from './QuickStartDemo'
Expand All @@ -43,6 +44,7 @@ const EXAMPLE_COMPONENTS: ExampleComponentType[] = [
RevealHeaderOnScroll,
RevealHeaderOnScrollSnap,
Lazy,
LazyNoFade,
ScrollableTabs,
CenteredEmptyList,
ScrollOnHeader,
Expand Down Expand Up @@ -167,7 +169,7 @@ const styles = StyleSheet.create({
statusbar: {
height: Platform.select({
android: Constants.statusBarHeight,
ios: Platform.Version < 11 ? Constants.statusBarHeight : 0,
ios: 0,
}),
},
appbar: {
Expand Down
17 changes: 17 additions & 0 deletions example/src/LazyNoFade.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import React from 'react'

import ExampleComponent from './Shared/ExampleComponent'
import { buildHeader } from './Shared/Header'
import { ExampleComponentType } from './types'

const title = 'Lazy without Fade In Example'

const Header = buildHeader(title)

const DefaultExample: ExampleComponentType = () => {
return <ExampleComponent renderHeader={Header} lazy cancelLazyFadeIn />
}

DefaultExample.title = title

export default DefaultExample
24 changes: 10 additions & 14 deletions src/Container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -435,20 +435,16 @@ export const Container = React.memo(
return (
<View key={i}>
<TabNameContext.Provider value={tabName}>
{lazy ? (
<Lazy
startMounted={i === index.value}
cancelLazyFadeIn={cancelLazyFadeIn}
>
{
React.Children.toArray(children)[
i
] as React.ReactElement
}
</Lazy>
) : (
React.Children.toArray(children)[i]
)}
<Lazy
startMounted={lazy ? undefined : true}
cancelLazyFadeIn={!lazy ? true : !!cancelLazyFadeIn}
>
{
React.Children.toArray(children)[
i
] as React.ReactElement
}
</Lazy>
</TabNameContext.Provider>
</View>
)
Expand Down
4 changes: 3 additions & 1 deletion src/FlashList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ function FlashListImpl<R>(

const { scrollHandler, enable } = useScrollHandlerY(name)

useAfterMountEffect(() => {
const onLayout = useAfterMountEffect(rest.onLayout, () => {
'worklet'
// we enable the scroll event after mounting
// otherwise we get an `onScroll` call with the initial scroll position which can break things
enable(true)
Expand Down Expand Up @@ -138,6 +139,7 @@ function FlashListImpl<R>(
// @ts-expect-error typescript complains about `unknown` in the memo, it should be T
<FlashListMemo
{...rest}
onLayout={onLayout}
ref={refWorkaround}
contentContainerStyle={memoContentContainerStyle}
bouncesZoom={false}
Expand Down
4 changes: 3 additions & 1 deletion src/FlatList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ function FlatListImpl<R>(
const ref = useSharedAnimatedRef<RNFlatList<unknown>>(passRef)

const { scrollHandler, enable } = useScrollHandlerY(name)
useAfterMountEffect(() => {
const onLayout = useAfterMountEffect(rest.onLayout, () => {
'worklet'
// we enable the scroll event after mounting
// otherwise we get an `onScroll` call with the initial scroll position which can break things
enable(true)
Expand Down Expand Up @@ -103,6 +104,7 @@ function FlatListImpl<R>(
// @ts-expect-error typescript complains about `unknown` in the memo, it should be T
<FlatListMemo
{...rest}
onLayout={onLayout}
ref={ref}
bouncesZoom={false}
style={memoStyle}
Expand Down
99 changes: 60 additions & 39 deletions src/Lazy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,46 +15,80 @@ import { useScroller, useTabNameContext, useTabsContext } from './hooks'
* Typically used internally, but if you want to mix lazy and regular screens you can wrap the lazy ones with this component.
*/
export const Lazy: React.FC<{
startMounted?: boolean
/**
* Whether to cancel the lazy fade in animation. Defaults to false.
*/
cancelLazyFadeIn?: boolean
/**
* How long to wait before mounting the children.
*/
mountDelayMs?: number
/**
* Whether to start mounted. Defaults to true if we are the focused tab.
*/
startMounted?: boolean
children: React.ReactElement
}> = ({ children, startMounted, cancelLazyFadeIn }) => {
}> = ({
children,
cancelLazyFadeIn,
startMounted: _startMounted,
mountDelayMs = 50,
}) => {
const name = useTabNameContext()
const {
focusedTab,
refMap,
scrollY,
scrollYCurrent,
tabNames,
} = useTabsContext()
const [canMount, setCanMount] = React.useState(!!startMounted)
const [afterMount, setAfterMount] = React.useState(!!startMounted)
const { focusedTab, refMap } = useTabsContext()

/**
* We start mounted if we are the focused tab, or if props.startMounted is true.
*/
const startMounted = useSharedValue(
typeof _startMounted === 'boolean'
? _startMounted
: focusedTab.value === name
)

/**
* We keep track of whether a layout has been triggered
*/
const didTriggerLayout = useSharedValue(false)

/**
* This is used to control when children are mounted
*/
const [canMount, setCanMount] = React.useState(!!startMounted.value)
/**
* Ensure we don't mount after the component has been unmounted
*/
const isSelfMounted = React.useRef(true)

const opacity = useSharedValue(cancelLazyFadeIn || startMounted ? 1 : 0)
const opacity = useSharedValue(cancelLazyFadeIn || startMounted.value ? 1 : 0)

React.useEffect(() => {
return () => {
isSelfMounted.current = false
}
}, [])

const allowToMount = React.useCallback(() => {
// wait the scene to be at least 50 ms focused, before mounting
const startMountTimer = React.useCallback(() => {
// wait the scene to be at least mountDelay ms focused, before mounting
setTimeout(() => {
if (focusedTab.value === name) {
if (isSelfMounted.current) setCanMount(true)
}
}, 50)
}, [focusedTab.value, name])
}, mountDelayMs)
}, [focusedTab.value, mountDelayMs, name])

useAnimatedReaction(
() => {
return focusedTab.value === name
},
(focused) => {
if (focused && !canMount) {
runOnJS(allowToMount)()
(focused, wasFocused) => {
if (focused && !wasFocused && !canMount) {
if (cancelLazyFadeIn) {
opacity.value = 1
runOnJS(setCanMount)(true)
} else {
runOnJS(startMountTimer)()
}
}
},
[canMount, focusedTab]
Expand All @@ -66,27 +100,16 @@ export const Lazy: React.FC<{

useAnimatedReaction(
() => {
return afterMount
return didTriggerLayout.value
},
(isMounted, wasMounted) => {
if (isMounted && !wasMounted) {
const tabIndex = tabNames.value.findIndex((n) => n === name)
if (ref && tabIndex >= 0) {
scrollTo(
ref,
0,
typeof scrollY.value[tabIndex] === 'number'
? scrollY.value[tabIndex]
: scrollYCurrent.value,
false,
`[${name}] lazy sync`
)
}
if (!cancelLazyFadeIn && opacity.value !== 1)
if (!cancelLazyFadeIn && opacity.value !== 1) {
opacity.value = withTiming(1)
}
}
},
[ref, cancelLazyFadeIn, name, afterMount, scrollTo]
[ref, cancelLazyFadeIn, name, didTriggerLayout, scrollTo]
)

const stylez = useAnimatedStyle(() => {
Expand All @@ -96,18 +119,16 @@ export const Lazy: React.FC<{
}, [])

const onLayout = useCallback(() => {
setTimeout(() => {
setAfterMount(true)
}, 100)
}, [])
didTriggerLayout.value = true
}, [didTriggerLayout])

return canMount ? (
cancelLazyFadeIn ? (
children
) : (
<Animated.View
pointerEvents="box-none"
style={[styles.container, !cancelLazyFadeIn && stylez]}
style={[styles.container, !cancelLazyFadeIn ? stylez : undefined]}
onLayout={onLayout}
>
{children}
Expand Down
4 changes: 3 additions & 1 deletion src/ScrollView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ export const ScrollView = React.forwardRef<
progressViewOffset,
} = useCollapsibleStyle()
const { scrollHandler, enable } = useScrollHandlerY(name)
useAfterMountEffect(() => {
const onLayout = useAfterMountEffect(rest.onLayout, () => {
'worklet'
// we enable the scroll event after mounting
// otherwise we get an `onScroll` call with the initial scroll position which can break things
enable(true)
Expand Down Expand Up @@ -114,6 +115,7 @@ export const ScrollView = React.forwardRef<
return (
<ScrollViewMemo
{...rest}
onLayout={onLayout}
ref={ref}
bouncesZoom={false}
style={memoStyle}
Expand Down
4 changes: 3 additions & 1 deletion src/SectionList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ function SectionListImpl<R>(
const ref = useSharedAnimatedRef<RNSectionList<unknown>>(passRef)

const { scrollHandler, enable } = useScrollHandlerY(name)
useAfterMountEffect(() => {
const onLayout = useAfterMountEffect(rest.onLayout, () => {
'worklet'
// we enable the scroll event after mounting
// otherwise we get an `onScroll` call with the initial scroll position which can break things
enable(true)
Expand Down Expand Up @@ -110,6 +111,7 @@ function SectionListImpl<R>(
// @ts-expect-error typescript complains about `unknown` in the memo, it should be T
<SectionListMemo
{...rest}
onLayout={onLayout}
ref={ref}
bouncesZoom={false}
style={memoStyle}
Expand Down
Loading

0 comments on commit 7c0ec48

Please sign in to comment.