mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-06-03 05:34:23 -04:00
Add tanstack virtual
This commit is contained in:
parent
ff154b03f3
commit
d1816e2d7b
@ -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",
|
||||||
|
@ -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}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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";
|
||||||
};
|
};
|
||||||
|
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user