Add tanstack virtual

This commit is contained in:
Zoe Roux 2023-12-14 23:59:41 +01:00
parent ff154b03f3
commit d1816e2d7b
No known key found for this signature in database
4 changed files with 79 additions and 26 deletions

View File

@ -22,6 +22,7 @@
"@radix-ui/react-select": "^2.0.0", "@radix-ui/react-select": "^2.0.0",
"@tanstack/react-query": "^5.12.1", "@tanstack/react-query": "^5.12.1",
"@tanstack/react-query-devtools": "^5.12.2", "@tanstack/react-query-devtools": "^5.12.2",
"@tanstack/react-virtual": "^3.0.1",
"array-shuffle": "^3.0.0", "array-shuffle": "^3.0.0",
"expo-linear-gradient": "^12.5.0", "expo-linear-gradient": "^12.5.0",
"expo-modules-core": "^1.5.12", "expo-modules-core": "^1.5.12",

View File

@ -33,22 +33,23 @@ import {
import { Stylable, nativeStyleToCss, useYoshiki, ysMap } from "yoshiki"; import { Stylable, nativeStyleToCss, useYoshiki, ysMap } from "yoshiki";
import { EmptyView, ErrorView, Layout, WithLoading, addHeader } from "./fetch"; import { EmptyView, ErrorView, Layout, WithLoading, addHeader } from "./fetch";
import type { ContentStyle } from "@shopify/flash-list"; import type { ContentStyle } from "@shopify/flash-list";
import { useVirtualizer } from "@tanstack/react-virtual";
const InfiniteScroll = <Props,>({ const InfiniteScroll = <T extends { id: string }, Props>({
children, data,
loader, renderItem,
layout, layout,
loadMore, loadMore,
hasMore = true, hasMore,
isFetching, isFetching,
Header, Header,
headerProps, headerProps,
fetchMore = true,
contentContainerStyle, contentContainerStyle,
getItemSize,
...props ...props
}: { }: {
children?: ReactElement | (ReactElement | null)[] | null; data: T[];
loader?: (ReactElement | null)[]; renderItem: (item: T, index: number) => ReactElement;
layout: Layout; layout: Layout;
loadMore: () => void; loadMore: () => void;
hasMore: boolean; hasMore: boolean;
@ -57,12 +58,21 @@ const InfiniteScroll = <Props,>({
headerProps?: Props; headerProps?: Props;
fetchMore?: boolean; fetchMore?: boolean;
contentContainerStyle?: ContentStyle; contentContainerStyle?: ContentStyle;
getItemSize: (x: T, idx: number) => number;
} & Stylable) => { } & Stylable) => {
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const virtualizer = useVirtualizer({
getScrollElement: () => ref.current,
horizontal: layout.layout === "horizontal",
count: data.length + +hasMore,
estimateSize: (i) => (i === data.length ? 0 : getItemSize(data[i], i)),
overscan: 5,
});
const { css } = useYoshiki(); const { css } = useYoshiki();
const onScroll = useCallback(() => { const onScroll = useCallback(() => {
if (!ref.current || !hasMore || isFetching || !fetchMore) return; if (!ref.current || !hasMore || isFetching) return;
const scroll = const scroll =
layout.layout === "horizontal" layout.layout === "horizontal"
? ref.current.scrollWidth - ref.current.scrollLeft ? ref.current.scrollWidth - ref.current.scrollLeft
@ -72,7 +82,7 @@ const InfiniteScroll = <Props,>({
// Load more if less than 3 element's worth of scroll is left // Load more if less than 3 element's worth of scroll is left
if (scroll <= offset * 3) loadMore(); if (scroll <= offset * 3) loadMore();
}, [hasMore, isFetching, layout, loadMore, fetchMore]); }, [hasMore, isFetching, layout, loadMore]);
const scrollProps = { ref, onScroll }; const scrollProps = { ref, onScroll };
// Automatically trigger a scroll check on start and after a fetch end in case the user is already // Automatically trigger a scroll check on start and after a fetch end in case the user is already
@ -87,6 +97,7 @@ const InfiniteScroll = <Props,>({
[ [
{ {
display: "grid", display: "grid",
height: `${virtualizer.getTotalSize()}px`,
gridAutoRows: "max-content", gridAutoRows: "max-content",
// the as any is due to differencies between css types of native and web (already accounted for in yoshiki) // the as any is due to differencies between css types of native and web (already accounted for in yoshiki)
gridGap: layout.gap as any, gridGap: layout.gap as any,
@ -118,8 +129,20 @@ const InfiniteScroll = <Props,>({
nativeStyleToCss(props), nativeStyleToCss(props),
)} )}
> >
{children} {virtualizer.getVirtualItems().map((x) => (
{isFetching && loader} <div
key={data[x.index].id}
style={{
position: "absolute",
top: 0,
left: 0,
height: `${x.size}px`,
transform: `translateY(${x.start}px)`,
}}
>
{renderItem(data[x.index], x.index)}
</div>
))}
</div> </div>
); );
@ -139,7 +162,12 @@ const InfiniteScroll = <Props,>({
); );
}; };
export const InfiniteFetchList = <Data, _, HeaderProps, Kind extends number | string>({ export const InfiniteFetchList = <
Data extends { id: string },
_,
HeaderProps,
Kind extends number | string,
>({
query, query,
incremental = false, incremental = false,
placeholderCount = 2, placeholderCount = 2,
@ -151,6 +179,7 @@ export const InfiniteFetchList = <Data, _, HeaderProps, Kind extends number | st
headerProps, headerProps,
getItemType, getItemType,
getItemSize, getItemSize,
fetchMore = true,
nested, nested,
...props ...props
}: { }: {
@ -173,7 +202,7 @@ export const InfiniteFetchList = <Data, _, HeaderProps, Kind extends number | st
nested?: boolean; nested?: boolean;
}): JSX.Element | null => { }): JSX.Element | null => {
const oldItems = useRef<Data[] | undefined>(); const oldItems = useRef<Data[] | undefined>();
const { items, error, fetchNextPage, hasNextPage, isFetching } = query; let { items, error, fetchNextPage, hasNextPage, isFetching } = query;
if (incremental && items) oldItems.current = items; if (incremental && items) oldItems.current = items;
if (error) return addHeader(Header, <ErrorView error={error} />, headerProps); if (error) return addHeader(Header, <ErrorView error={error} />, headerProps);
@ -182,29 +211,32 @@ export const InfiniteFetchList = <Data, _, HeaderProps, Kind extends number | st
return addHeader(Header, <EmptyView message={empty} />, headerProps); return addHeader(Header, <EmptyView message={empty} />, headerProps);
} }
if (incremental) items ??= oldItems.current;
const placeholders = [...Array(placeholderCount)].map(
(_, i) => ({ id: `gen${i}`, isLoading: true }) as unknown as Data,
);
return ( return (
<InfiniteScroll <InfiniteScroll
layout={layout} layout={layout}
loadMore={fetchNextPage} loadMore={fetchNextPage}
hasMore={hasNextPage!} hasMore={fetchMore && hasNextPage!}
isFetching={isFetching} isFetching={isFetching}
loader={[...Array(placeholderCount)].map((_, i) => (
<Fragment key={i.toString()}>
{Divider && i !== 0 && (Divider === true ? <HR /> : <Divider />)}
{children({ isLoading: true } as any, i)}
</Fragment>
))}
Header={Header} Header={Header}
headerProps={headerProps} headerProps={headerProps}
{...props} data={isFetching ? [...(items || []), ...placeholders] : items ?? []}
> renderItem={(item, i) => (
{(items ?? oldItems.current)?.map((item, i) => (
<Fragment key={(item as any).id}> <Fragment key={(item as any).id}>
{Divider && i !== 0 && (Divider === true ? <HR /> : <Divider />)} {Divider && i !== 0 && (Divider === true ? <HR /> : <Divider />)}
{children({ ...item, isLoading: false } as any, i)} {children({ ...item, isLoading: false } as any, i)}
</Fragment> </Fragment>
))} )}
</InfiniteScroll> getItemSize={(x, i) =>
getItemSize && getItemType
? getItemSize(getItemType({ isLoading: false, ...x }, i))
: layout.size
}
{...props}
/>
); );
}; };

View File

@ -27,7 +27,7 @@ import { useYoshiki } from "yoshiki/native";
export type Layout = { export type Layout = {
numColumns: Breakpoint<number>; numColumns: Breakpoint<number>;
size: Breakpoint<number>; size: number;
gap: Breakpoint<number>; gap: Breakpoint<number>;
layout: "grid" | "horizontal" | "vertical"; layout: "grid" | "horizontal" | "vertical";
}; };

View File

@ -4741,6 +4741,25 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@tanstack/react-virtual@npm:^3.0.1":
version: 3.0.1
resolution: "@tanstack/react-virtual@npm:3.0.1"
dependencies:
"@tanstack/virtual-core": 3.0.0
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
checksum: 11534a23100de14a7e0a95da667381181e60a24e29a71246aeed174f8d5f6e176216de6639e6e1f403722ae30d8b92b21ed75ea131529b1417fb81f433468ef0
languageName: node
linkType: hard
"@tanstack/virtual-core@npm:3.0.0":
version: 3.0.0
resolution: "@tanstack/virtual-core@npm:3.0.0"
checksum: 7283d50fc7b7a56608c37a8e94a93b85890ff7e39c6281633a19c4d6f6f4fbf25f8418f1eec302a008a8746a0d1d0cd00630137b55e6cf019818d68af8ed16b6
languageName: node
linkType: hard
"@tootallnate/once@npm:2": "@tootallnate/once@npm:2":
version: 2.0.0 version: 2.0.0
resolution: "@tootallnate/once@npm:2.0.0" resolution: "@tootallnate/once@npm:2.0.0"
@ -15339,6 +15358,7 @@ __metadata:
"@svgr/webpack": ^8.1.0 "@svgr/webpack": ^8.1.0
"@tanstack/react-query": ^5.12.1 "@tanstack/react-query": ^5.12.1
"@tanstack/react-query-devtools": ^5.12.2 "@tanstack/react-query-devtools": ^5.12.2
"@tanstack/react-virtual": ^3.0.1
"@types/node": 20.10.1 "@types/node": 20.10.1
"@types/react": 18.2.39 "@types/react": 18.2.39
"@types/react-dom": 18.2.17 "@types/react-dom": 18.2.17