diff --git a/src/MasonryFlashList b/src/MasonryFlashList new file mode 100644 index 0000000..83218da --- /dev/null +++ b/src/MasonryFlashList @@ -0,0 +1,165 @@ +import type { + MasonryFlashListProps, + MasonryFlashList as SPMasonryFlashList, +} from '@shopify/flash-list' +import React, { useCallback } from 'react' +import Animated from 'react-native-reanimated' + +import { + useAfterMountEffect, + useChainCallback, + useCollapsibleStyle, + useConvertAnimatedToValue, + useScrollHandlerY, + useSharedAnimatedRef, + useTabNameContext, + useTabsContext, + useUpdateScrollViewContentSize, +} from './hooks' + +/** + * Used as a memo to prevent rerendering too often when the context changes. + * See: https://github.com/facebook/react/issues/15156#issuecomment-474590693 + */ + +type MasonryFlashListMemoProps = React.PropsWithChildren> +type MasonryFlashListMemoRef = SPMasonryFlashList + +let AnimatedMasonryFlashList: React.ComponentClass> | null = null + +const ensureFlastList = () => { + if (AnimatedMasonryFlashList) { + return + } + + try { + const MasonryFlashListModule = require('@shopify/flash-list') + AnimatedMasonryFlashList = (Animated.createAnimatedComponent( + MasonryFlashListModule.MasonryFlashList + ) as unknown) as React.ComponentClass> + } catch (error) { + console.error( + 'The optional dependency @shopify/flash-list is not installed. Please install it to use the MasonryFlashList component.' + ) + } +} + +const MasonryFlashListMemo = React.memo( + React.forwardRef((props, passRef) => { + ensureFlastList() + return AnimatedMasonryFlashList ? ( + + ) : ( + <> + ) + }) +) + +function MasonryFlashListImpl( + { + style, + onContentSizeChange, + refreshControl, + contentContainerStyle: _contentContainerStyle, + ...rest + }: Omit, 'onScroll'>, + passRef: React.Ref> +) { + const name = useTabNameContext() + const { setRef, contentInset } = useTabsContext() + const ref = useSharedAnimatedRef(passRef) + const recyclerRef = useSharedAnimatedRef(null) + + const { scrollHandler, enable } = useScrollHandlerY(name) + + 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) + }) + + const { progressViewOffset, contentContainerStyle } = useCollapsibleStyle() + + React.useEffect(() => { + setRef(name, recyclerRef) + }, [name, recyclerRef, setRef]) + + const scrollContentSizeChange = useUpdateScrollViewContentSize({ + name, + }) + + const scrollContentSizeChangeHandlers = useChainCallback( + React.useMemo(() => [scrollContentSizeChange, onContentSizeChange], [ + onContentSizeChange, + scrollContentSizeChange, + ]) + ) + + const memoRefreshControl = React.useMemo( + () => + refreshControl && + React.cloneElement(refreshControl, { + progressViewOffset, + ...refreshControl.props, + }), + [progressViewOffset, refreshControl] + ) + + const contentInsetValue = useConvertAnimatedToValue(contentInset) + + const memoContentInset = React.useMemo(() => ({ top: contentInsetValue }), [ + contentInsetValue, + ]) + + const memoContentOffset = React.useMemo( + () => ({ x: 0, y: -contentInsetValue }), + [contentInsetValue] + ) + + const memoContentContainerStyle = React.useMemo( + () => ({ + paddingTop: contentContainerStyle.paddingTop, + ..._contentContainerStyle, + }), + [_contentContainerStyle, contentContainerStyle.paddingTop] + ) + + const refWorkaround = useCallback( + (value: MasonryFlashListMemoRef | null): void => { + // https://github.com/Shopify/flash-list/blob/2d31530ed447a314ec5429754c7ce88dad8fd087/src/MasonryFlashList.tsx#L829 + // We are not accessing the right element or view of the MasonryFlashList (recyclerlistview). So we need to give + // this ref the access to it + // eslint-ignore + ;(recyclerRef as any)(value?.recyclerlistview_unsafe) + ;(ref as any)(value) + }, + [recyclerRef, ref] + ) + + return ( + // @ts-expect-error typescript complains about `unknown` in the memo, it should be T + + ) +} + +/** + * Use like a regular MasonryFlashList. + */ +export const MasonryFlashList = React.forwardRef(MasonryFlashListImpl) as ( + p: MasonryFlashListProps & { ref?: React.Ref> } +) => React.ReactElement diff --git a/src/index.tsx b/src/index.tsx index 019eb4b..460b2a9 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,6 +1,7 @@ import { Container } from './Container' import { FlashList } from './FlashList' import { FlatList } from './FlatList' +import { MasonryFlashList } from './MasonryFlashList' import { Lazy } from './Lazy' import { MaterialTabBarProps, MaterialTabItemProps } from './MaterialTabBar' import { ScrollView } from './ScrollView' @@ -35,6 +36,7 @@ export const Tabs = { Tab, Lazy, FlatList, + MasonryFlashList, ScrollView, SectionList, FlashList,