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",
"@tanstack/react-query": "^5.12.1",
"@tanstack/react-query-devtools": "^5.12.2",
"@tanstack/react-virtual": "^3.0.1",
"array-shuffle": "^3.0.0",
"expo-linear-gradient": "^12.5.0",
"expo-modules-core": "^1.5.12",

View File

@ -33,22 +33,23 @@ import {
import { Stylable, nativeStyleToCss, useYoshiki, ysMap } from "yoshiki";
import { EmptyView, ErrorView, Layout, WithLoading, addHeader } from "./fetch";
import type { ContentStyle } from "@shopify/flash-list";
import { useVirtualizer } from "@tanstack/react-virtual";
const InfiniteScroll = <Props,>({
children,
loader,
const InfiniteScroll = <T extends { id: string }, Props>({
data,
renderItem,
layout,
loadMore,
hasMore = true,
hasMore,
isFetching,
Header,
headerProps,
fetchMore = true,
contentContainerStyle,
getItemSize,
...props
}: {
children?: ReactElement | (ReactElement | null)[] | null;
loader?: (ReactElement | null)[];
data: T[];
renderItem: (item: T, index: number) => ReactElement;
layout: Layout;
loadMore: () => void;
hasMore: boolean;
@ -57,12 +58,21 @@ const InfiniteScroll = <Props,>({
headerProps?: Props;
fetchMore?: boolean;
contentContainerStyle?: ContentStyle;
getItemSize: (x: T, idx: number) => number;
} & Stylable) => {
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 onScroll = useCallback(() => {
if (!ref.current || !hasMore || isFetching || !fetchMore) return;
if (!ref.current || !hasMore || isFetching) return;
const scroll =
layout.layout === "horizontal"
? 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
if (scroll <= offset * 3) loadMore();
}, [hasMore, isFetching, layout, loadMore, fetchMore]);
}, [hasMore, isFetching, layout, loadMore]);
const scrollProps = { ref, onScroll };
// 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",
height: `${virtualizer.getTotalSize()}px`,
gridAutoRows: "max-content",
// the as any is due to differencies between css types of native and web (already accounted for in yoshiki)
gridGap: layout.gap as any,
@ -118,8 +129,20 @@ const InfiniteScroll = <Props,>({
nativeStyleToCss(props),
)}
>
{children}
{isFetching && loader}
{virtualizer.getVirtualItems().map((x) => (
<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>
);
@ -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,
incremental = false,
placeholderCount = 2,
@ -151,6 +179,7 @@ export const InfiniteFetchList = <Data, _, HeaderProps, Kind extends number | st
headerProps,
getItemType,
getItemSize,
fetchMore = true,
nested,
...props
}: {
@ -173,7 +202,7 @@ export const InfiniteFetchList = <Data, _, HeaderProps, Kind extends number | st
nested?: boolean;
}): JSX.Element | null => {
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 (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);
}
if (incremental) items ??= oldItems.current;
const placeholders = [...Array(placeholderCount)].map(
(_, i) => ({ id: `gen${i}`, isLoading: true }) as unknown as Data,
);
return (
<InfiniteScroll
layout={layout}
loadMore={fetchNextPage}
hasMore={hasNextPage!}
hasMore={fetchMore && hasNextPage!}
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}
headerProps={headerProps}
{...props}
>
{(items ?? oldItems.current)?.map((item, i) => (
data={isFetching ? [...(items || []), ...placeholders] : items ?? []}
renderItem={(item, i) => (
<Fragment key={(item as any).id}>
{Divider && i !== 0 && (Divider === true ? <HR /> : <Divider />)}
{children({ ...item, isLoading: false } as any, i)}
</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 = {
numColumns: Breakpoint<number>;
size: Breakpoint<number>;
size: number;
gap: Breakpoint<number>;
layout: "grid" | "horizontal" | "vertical";
};

View File

@ -4741,6 +4741,25 @@ __metadata:
languageName: node
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":
version: 2.0.0
resolution: "@tootallnate/once@npm:2.0.0"
@ -15339,6 +15358,7 @@ __metadata:
"@svgr/webpack": ^8.1.0
"@tanstack/react-query": ^5.12.1
"@tanstack/react-query-devtools": ^5.12.2
"@tanstack/react-virtual": ^3.0.1
"@types/node": 20.10.1
"@types/react": 18.2.39
"@types/react-dom": 18.2.17