Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding MasonryFlashList #351

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
165 changes: 165 additions & 0 deletions src/MasonryFlashList
Original file line number Diff line number Diff line change
@@ -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<MasonryFlashListProps<unknown>>
type MasonryFlashListMemoRef = SPMasonryFlashList<any>

let AnimatedMasonryFlashList: React.ComponentClass<MasonryFlashListProps<any>> | null = null

const ensureFlastList = () => {
if (AnimatedMasonryFlashList) {
return
}

try {
const MasonryFlashListModule = require('@shopify/flash-list')
AnimatedMasonryFlashList = (Animated.createAnimatedComponent(
MasonryFlashListModule.MasonryFlashList
) as unknown) as React.ComponentClass<MasonryFlashListProps<any>>
} 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<MasonryFlashListMemoRef, MasonryFlashListMemoProps>((props, passRef) => {
ensureFlastList()
return AnimatedMasonryFlashList ? (
<AnimatedMasonryFlashList ref={passRef} {...props} />
) : (
<></>
)
})
)

function MasonryFlashListImpl<R>(
{
style,
onContentSizeChange,
refreshControl,
contentContainerStyle: _contentContainerStyle,
...rest
}: Omit<MasonryFlashListProps<R>, 'onScroll'>,
passRef: React.Ref<SPMasonryFlashList<any>>
) {
const name = useTabNameContext()
const { setRef, contentInset } = useTabsContext()
const ref = useSharedAnimatedRef<any>(passRef)
const recyclerRef = useSharedAnimatedRef<any>(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<number>(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
<MasonryFlashListMemo
{...rest}
onLayout={onLayout}
ref={refWorkaround}
contentContainerStyle={memoContentContainerStyle}
bouncesZoom={false}
onScroll={scrollHandler}
scrollEventThrottle={16}
contentInset={memoContentInset}
contentOffset={memoContentOffset}
refreshControl={memoRefreshControl}
progressViewOffset={progressViewOffset}
automaticallyAdjustContentInsets={false}
onContentSizeChange={scrollContentSizeChangeHandlers}
/>
)
}

/**
* Use like a regular MasonryFlashList.
*/
export const MasonryFlashList = React.forwardRef(MasonryFlashListImpl) as <T>(
p: MasonryFlashListProps<T> & { ref?: React.Ref<SPMasonryFlashList<T>> }
) => React.ReactElement
2 changes: 2 additions & 0 deletions src/index.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -35,6 +36,7 @@ export const Tabs = {
Tab,
Lazy,
FlatList,
MasonryFlashList,
ScrollView,
SectionList,
FlashList,
Expand Down