refactor: upgrade FlashList to v2 (#4298)

* refactor: upgrade FlashList to v2

* chore: upgrade @shopify/flash-list to v2.0.0

* chore: update packageManager version to pnpm@10.14.0

* fix: revert packageManager version to pnpm@10.12.1

* chore: update ref type for TimelineSelectorMasonryList component
This commit is contained in:
Whitewater 2025-08-03 13:56:39 +08:00 committed by GitHub
parent 7a4f5078e7
commit 3e04dc7291
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 50 additions and 79 deletions

View File

@ -45,7 +45,7 @@
"@react-native-masked-view/masked-view": "0.3.2",
"@react-native-menu/menu": "1.2.3",
"@react-native-picker/picker": "2.11.1",
"@shopify/flash-list": "1.8.3",
"@shopify/flash-list": "2.0.0",
"@tanstack/query-sync-storage-persister": "5.81.5",
"@tanstack/react-query": "5.81.5",
"@tanstack/react-query-persist-client": "5.81.5",

View File

@ -1,12 +1,12 @@
import { RSSHubCategories } from "@follow/constants"
import type { RSSHubRouteDeclaration } from "@follow/models/rsshub"
import { isASCII } from "@follow/utils"
import type { ContentStyle } from "@shopify/flash-list"
import type { FlashListRef } from "@shopify/flash-list"
import { FlashList } from "@shopify/flash-list"
import { useQuery } from "@tanstack/react-query"
import { useEffect, useMemo, useRef, useState } from "react"
import { useTranslation } from "react-i18next"
import type { ScrollView } from "react-native"
import type { ScrollView, StyleProp, ViewStyle } from "react-native"
import { Animated, useAnimatedValue, useWindowDimensions, View } from "react-native"
import type { SharedValue } from "react-native-reanimated"
import { useSharedValue } from "react-native-reanimated"
@ -137,7 +137,7 @@ const LanguageMap = {
cmn: "zh-CN",
} as const
export const RecommendationTab: TabComponent<{
contentContainerStyle?: ContentStyle
contentContainerStyle?: StyleProp<ViewStyle>
insets?: {
top?: number
bottom?: number
@ -231,7 +231,7 @@ export const RecommendationTab: TabComponent<{
// Add ref for FlashList
const listRef = useRegisterNavigationScrollView<
FlashList<{
FlashListRef<{
key: string
data: RSSHubRouteDeclaration
}>
@ -287,7 +287,6 @@ export const RecommendationTab: TabComponent<{
}
}}
scrollEventThrottle={16}
estimatedItemSize={150}
ref={listRef}
data={alphabetGroups}
keyExtractor={keyExtractor}

View File

@ -1,6 +1,6 @@
import type { FeedViewType } from "@follow/constants"
import { usePrefetchEntryTranslation } from "@follow/store/translation/hooks"
import type { FlashList, ListRenderItemInfo } from "@shopify/flash-list"
import type { FlashListRef, ListRenderItemInfo } from "@shopify/flash-list"
import type { ElementRef } from "react"
import { useCallback, useImperativeHandle, useMemo, useRef } from "react"
import { View } from "react-native"
@ -43,7 +43,7 @@ export const EntryListContentArticle = ({
[hasNextPage, fetchedTime],
)
const ref = useRef<FlashList<any>>(null)
const ref = useRef<FlashListRef<any>>(null)
const { onViewableItemsChanged, onScroll, viewableItems } = useOnViewableItemsChanged({
disabled: active === false || isFetching,
@ -82,7 +82,6 @@ export const EntryListContentArticle = ({
data={entryIds}
extraData={extraData}
keyExtractor={defaultKeyExtractor}
estimatedItemSize={100}
renderItem={renderItem}
onEndReached={fetchNextPage}
onScroll={onScroll}

View File

@ -1,6 +1,6 @@
import { useTypeScriptHappyCallback } from "@follow/hooks"
import { usePrefetchEntryTranslation } from "@follow/store/translation/hooks"
import type { FlashList, MasonryFlashListProps } from "@shopify/flash-list"
import type { FlashListProps, FlashListRef } from "@shopify/flash-list"
import type { ElementRef } from "react"
import { useImperativeHandle, useRef } from "react"
import { StyleSheet, View } from "react-native"
@ -24,10 +24,10 @@ export const EntryListContentPicture = ({
active,
...rest
}: { entryIds: string[] | null; active?: boolean } & Omit<
MasonryFlashListProps<string>,
FlashListProps<string>,
"data" | "renderItem"
> & { ref?: React.Ref<ElementRef<typeof TimelineSelectorMasonryList> | null> }) => {
const ref = useRef<FlashList<any>>(null)
const ref = useRef<FlashListRef<any>>(null)
useImperativeHandle(forwardRef, () => ref.current!)
const { fetchNextPage, refetch, isRefetching, hasNextPage, isFetching, isReady } = useEntries()
@ -84,7 +84,6 @@ export const EntryListContentPicture = ({
onScroll={onScroll}
onEndReached={fetchNextPage}
numColumns={2}
estimatedItemSize={100}
contentContainerStyle={styles.contentContainer}
ListFooterComponent={
hasNextPage ? (

View File

@ -1,5 +1,5 @@
import { usePrefetchEntryTranslation } from "@follow/store/translation/hooks"
import type { FlashList, ListRenderItemInfo } from "@shopify/flash-list"
import type { FlashListRef, ListRenderItemInfo } from "@shopify/flash-list"
import type { ElementRef } from "react"
import { useCallback, useImperativeHandle, useMemo, useRef } from "react"
import { View } from "react-native"
@ -25,7 +25,7 @@ export const EntryListContentSocial = ({
const { fetchNextPage, isFetching, refetch, isRefetching, hasNextPage, isReady } = useEntries()
const extraData: EntryExtraData = useMemo(() => ({ entryIds }), [entryIds])
const ref = useRef<FlashList<any>>(null)
const ref = useRef<FlashListRef<any>>(null)
useImperativeHandle(forwardRef, () => ref.current!)
// eslint-disable-next-line @eslint-react/hooks-extra/no-unnecessary-use-callback
const renderItem = useCallback(
@ -61,7 +61,6 @@ export const EntryListContentSocial = ({
isRefetching={false}
data={Array.from({ length: 5 }).map((_, index) => `skeleton-${index}`)}
keyExtractor={(id) => id}
estimatedItemSize={5}
renderItem={EntryItemSkeleton}
ItemSeparatorComponent={ItemSeparatorFullWidth}
/>
@ -78,7 +77,6 @@ export const EntryListContentSocial = ({
data={entryIds}
extraData={extraData}
keyExtractor={(id) => id}
estimatedItemSize={100}
renderItem={renderItem}
onEndReached={fetchNextPage}
onViewableItemsChanged={onViewableItemsChanged}

View File

@ -1,6 +1,6 @@
import { useTypeScriptHappyCallback } from "@follow/hooks"
import { usePrefetchEntryTranslation } from "@follow/store/translation/hooks"
import type { FlashList, MasonryFlashListProps } from "@shopify/flash-list"
import type { FlashListProps, FlashListRef } from "@shopify/flash-list"
import type { ElementRef } from "react"
import { useImperativeHandle, useMemo, useRef } from "react"
import { StyleSheet, View } from "react-native"
@ -22,10 +22,10 @@ export const EntryListContentVideo = ({
active,
...rest
}: { entryIds: string[] | null; active?: boolean } & Omit<
MasonryFlashListProps<string>,
FlashListProps<string>,
"data" | "renderItem"
> & { ref?: React.Ref<ElementRef<typeof TimelineSelectorMasonryList> | null> }) => {
const ref = useRef<FlashList<any>>(null)
const ref = useRef<FlashListRef<any>>(null)
useImperativeHandle(forwardRef, () => ref.current!)
const { fetchNextPage, refetch, isRefetching, isFetching, hasNextPage, isReady } = useEntries()
const { onViewableItemsChanged, onScroll, viewableItems } = useOnViewableItemsChanged({
@ -95,7 +95,6 @@ export const EntryListContentVideo = ({
onScroll={onScroll}
onEndReached={fetchNextPage}
numColumns={2}
estimatedItemSize={100}
ListFooterComponent={ListFooterComponent}
{...rest}
onRefresh={refetch}

View File

@ -1,6 +1,6 @@
import { FeedViewType } from "@follow/constants"
import { useWhoami } from "@follow/store/user/hooks"
import type { FlashList } from "@shopify/flash-list"
import type { FlashListRef } from "@shopify/flash-list"
import type { RefObject } from "react"
import { useEffect } from "react"
@ -30,7 +30,7 @@ type EntryListSelectorProps = {
}
function EntryListSelectorImpl({ entryIds, viewId, active = true }: EntryListSelectorProps) {
const ref = useRegisterNavigationScrollView<FlashList<any>>(active)
const ref = useRegisterNavigationScrollView<FlashListRef<any>>(active)
let ContentComponent:
| typeof EntryListContentSocial
@ -89,7 +89,7 @@ export const EntryListSelector = withErrorBoundary(
)
const useAutoScrollToEntryAfterPullUpToNext = (
ref: RefObject<FlashList<any> | null>,
ref: RefObject<FlashListRef<any> | null>,
entryIds: string[],
) => {
const navigation = useNavigation()

View File

@ -1,6 +1,6 @@
import { debouncedFetchEntryContentByStream } from "@follow/store/entry/store"
import { unreadSyncService } from "@follow/store/unread/store"
import type ViewToken from "@shopify/flash-list/dist/viewability/ViewToken"
import type { ViewToken } from "@shopify/flash-list"
import { fetch as expoFetch } from "expo/fetch"
import { useCallback, useEffect, useInsertionEffect, useMemo, useRef, useState } from "react"
import type { NativeScrollEvent, NativeSyntheticEvent } from "react-native"
@ -8,14 +8,14 @@ import type { NativeScrollEvent, NativeSyntheticEvent } from "react-native"
import { useGeneralSettingKey } from "@/src/atoms/settings/general"
import { getCookie } from "@/src/lib/auth"
const defaultIdExtractor = (item: ViewToken) => item.key
const defaultIdExtractor = (item: ViewToken<string>) => item.key
export function useOnViewableItemsChanged({
disabled,
idExtractor = defaultIdExtractor,
onScroll: onScrollProp,
}: {
disabled?: boolean
idExtractor?: (item: ViewToken) => string
idExtractor?: (item: ViewToken<string>) => string
onScroll?: (e: NativeSyntheticEvent<NativeScrollEvent>) => void
} = {}) {
const orientation = useRef<"down" | "up">("down")
@ -23,15 +23,15 @@ export function useOnViewableItemsChanged({
const markAsReadWhenScrolling = useGeneralSettingKey("scrollMarkUnread")
const markAsReadWhenRendering = useGeneralSettingKey("renderMarkUnread")
const [viewableItems, setViewableItems] = useState<ViewToken[]>([])
const [lastViewableItems, setLastViewableItems] = useState<ViewToken[] | null>()
const [lastRemovedItems, setLastRemovedItems] = useState<ViewToken[] | null>(null)
const [viewableItems, setViewableItems] = useState<ViewToken<string>[]>([])
const [lastViewableItems, setLastViewableItems] = useState<ViewToken<string>[] | null>()
const [lastRemovedItems, setLastRemovedItems] = useState<ViewToken<string>[] | null>(null)
const [stableIdExtractor] = useState(() => idExtractor)
const onViewableItemsChanged: (info: {
viewableItems: ViewToken[]
changed: ViewToken[]
viewableItems: ViewToken<string>[]
changed: ViewToken<string>[]
}) => void = useNonReactiveCallback(({ viewableItems, changed }) => {
setViewableItems(viewableItems)

View File

@ -2,14 +2,9 @@ import { useTypeScriptHappyCallback } from "@follow/hooks"
import { usePrefetchSubscription } from "@follow/store/subscription/hooks"
import { usePrefetchUnread } from "@follow/store/unread/hooks"
import { nextFrame } from "@follow/utils"
import type {
FlashListProps,
MasonryFlashListProps,
MasonryFlashListRef,
} from "@shopify/flash-list"
import { FlashList, MasonryFlashList } from "@shopify/flash-list"
import type { FlashListProps, FlashListRef } from "@shopify/flash-list"
import { FlashList } from "@shopify/flash-list"
import * as Haptics from "expo-haptics"
import type { ElementRef, RefObject } from "react"
import { use, useCallback, useImperativeHandle, useRef } from "react"
import type { NativeScrollEvent, NativeSyntheticEvent } from "react-native"
import { RefreshControl, View } from "react-native"
@ -32,8 +27,9 @@ export const TimelineSelectorList = ({
onRefresh,
isRefetching,
...props
}: Props & Omit<FlashListProps<any>, "onRefresh"> & { ref?: React.Ref<FlashList<any> | null> }) => {
const ref = useRef<FlashList<any>>(null)
}: Props &
Omit<FlashListProps<any>, "onRefresh"> & { ref?: React.Ref<FlashListRef<any> | null> }) => {
const ref = useRef<FlashListRef<any>>(null)
useImperativeHandle(forwardedRef, () => ref.current!)
const { refetch: unreadRefetch } = usePrefetchUnread()
const { refetch: subscriptionRefetch } = usePrefetchSubscription()
@ -118,8 +114,8 @@ export const TimelineSelectorMasonryList = ({
isRefetching,
...props
}: Props &
Omit<MasonryFlashListProps<any>, "onRefresh"> & {
ref?: React.Ref<ElementRef<typeof MasonryFlashList> | null>
Omit<FlashListProps<any>, "onRefresh"> & {
ref?: React.Ref<FlashListRef<any> | null>
}) => {
const { refetch: unreadRefetch } = usePrefetchUnread()
const { refetch: subscriptionRefetch } = usePrefetchSubscription()
@ -147,8 +143,9 @@ export const TimelineSelectorMasonryList = ({
}
return (
<MasonryFlashList
ref={ref as RefObject<MasonryFlashListRef<any>>}
<FlashList
ref={ref}
masonry
refreshControl={
<RefreshControl
progressViewOffset={headerHeight}
@ -163,11 +160,13 @@ export const TimelineSelectorMasonryList = ({
/>
}
{...props}
contentContainerStyle={{
paddingTop: headerHeight,
paddingBottom: tabBarHeight,
...props.contentContainerStyle,
}}
contentContainerStyle={[
{
paddingTop: headerHeight,
paddingBottom: tabBarHeight,
},
props.contentContainerStyle,
]}
scrollIndicatorInsets={{
top: headerHeight - insets.top,
bottom: tabBarHeight ? tabBarHeight - insets.bottom : undefined,

View File

@ -9,7 +9,7 @@ import {
useSortedUngroupedSubscription,
} from "@follow/store/subscription/hooks"
import { subscriptionSyncService } from "@follow/store/subscription/store"
import type { FlashList } from "@shopify/flash-list"
import type { FlashListRef } from "@shopify/flash-list"
import type { ParseKeys } from "i18next"
import { memo, useCallback, useMemo, useState } from "react"
import { useTranslation } from "react-i18next"
@ -127,7 +127,7 @@ const SubscriptionListImpl = ({
const onRefresh = useEventCallback(() => {
return subscriptionSyncService.fetch(view)
})
const scrollViewRef = useRegisterNavigationScrollView<FlashList<any> | null>(active)
const scrollViewRef = useRegisterNavigationScrollView<FlashListRef<any> | null>(active)
return (
<TimelineSelectorList
@ -141,7 +141,6 @@ const SubscriptionListImpl = ({
}}
isRefetching={refreshing}
data={data}
estimatedItemSize={50}
renderItem={ItemRender}
keyExtractor={keyExtractor}
extraData={extraData}

View File

@ -863,8 +863,8 @@ importers:
specifier: 2.11.1
version: 2.11.1(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0)
'@shopify/flash-list':
specifier: 1.8.3
version: 1.8.3(@babel/runtime@7.27.6)(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0)
specifier: 2.0.0
version: 2.0.0(@babel/runtime@7.27.6)(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0)
'@tanstack/query-sync-storage-persister':
specifier: 5.81.5
version: 5.81.5
@ -6229,8 +6229,8 @@ packages:
'@shikijs/vscode-textmate@10.0.2':
resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==}
'@shopify/flash-list@1.8.3':
resolution: {integrity: sha512-vXuj6JyuMjONVOXjEhWFeaONPuWN/53Cl2LeyeM8TZ0JzUcNU+BE6iyga1/yyJeDf0K7YPgAE/PcUX2+DM1LiA==}
'@shopify/flash-list@2.0.0':
resolution: {integrity: sha512-N04dXZq4wgrXx3nGytchR4FUN/hBO9PmvpcqMXvy4841zTL8GKLPbdwsFe8UdLsaM5n++mBPWft0wqEddW5Q/w==}
peerDependencies:
'@babel/runtime': '*'
react: 19.0.0
@ -13961,12 +13961,6 @@ packages:
resolution: {integrity: sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==}
engines: {node: '>= 10.13.0'}
recyclerlistview@4.2.3:
resolution: {integrity: sha512-STR/wj/FyT8EMsBzzhZ1l2goYirMkIgfV3gYEPxI3Kf3lOnu6f7Dryhyw7/IkQrgX5xtTcDrZMqytvteH9rL3g==}
peerDependencies:
react: 19.0.0
react-native: '>= 0.30.0'
redeyed@2.1.1:
resolution: {integrity: sha512-FNpGGo1DycYAdnrKFxCMmKYgo/mILAqtRYbkdQD8Ep/Hk2PQ5+aEAEx+IU713RTDmuBaH0c8P5ZozurNu5ObRQ==}
@ -15151,9 +15145,6 @@ packages:
'@swc/wasm':
optional: true
ts-object-utils@0.0.5:
resolution: {integrity: sha512-iV0GvHqOmilbIKJsfyfJY9/dNHCs969z3so90dQWsO1eMMozvTpnB1MEaUbb3FYtZTGjv5sIy/xmslEz0Rg2TA==}
ts-pattern@5.7.1:
resolution: {integrity: sha512-EGs8PguQqAAUIcQfK4E9xdXxB6s2GK4sJfT/vcc9V1ELIvC4LH/zXu2t/5fajtv6oiRCxdv7BgtVK3vWgROxag==}
@ -22090,13 +22081,11 @@ snapshots:
'@shikijs/vscode-textmate@10.0.2': {}
'@shopify/flash-list@1.8.3(@babel/runtime@7.27.6)(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0)':
'@shopify/flash-list@2.0.0(@babel/runtime@7.27.6)(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0)':
dependencies:
'@babel/runtime': 7.27.6
react: 19.0.0
react-native: 0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)
recyclerlistview: 4.2.3(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0)
tslib: 2.8.1
'@shuding/opentype.js@1.4.0-beta.0':
dependencies:
@ -31236,14 +31225,6 @@ snapshots:
dependencies:
resolve: 1.22.10
recyclerlistview@4.2.3(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0):
dependencies:
lodash.debounce: 4.0.8
prop-types: 15.8.1
react: 19.0.0
react-native: 0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)
ts-object-utils: 0.0.5
redeyed@2.1.1:
dependencies:
esprima: 4.0.1
@ -32645,8 +32626,6 @@ snapshots:
v8-compile-cache-lib: 3.0.1
yn: 3.1.1
ts-object-utils@0.0.5: {}
ts-pattern@5.7.1: {}
ts-tiny-invariant@2.0.5: {}