refactor: add dynamic virtual list

This commit is contained in:
moonrailgun 2024-07-14 00:17:58 +08:00
parent 15c6290587
commit 01d81f3929
3 changed files with 125 additions and 20 deletions

View File

@ -0,0 +1,103 @@
import { useWatch } from '@/hooks/useWatch';
import { useTranslation } from '@i18next-toolkit/react';
import { useVirtualizer } from '@tanstack/react-virtual';
import { Empty } from 'antd';
import { last } from 'lodash-es';
import React, { useRef } from 'react';
interface VirtualListProps<T = any> {
allData: T[];
hasNextPage: boolean | undefined;
isFetchingNextPage: boolean;
onFetchNextPage: () => void;
estimateSize: number;
renderItem: (item: T) => React.ReactElement;
renderEmpty?: () => React.ReactElement;
}
export const DynamicVirtualList: React.FC<VirtualListProps> = React.memo(
(props) => {
const {
allData,
hasNextPage,
isFetchingNextPage,
onFetchNextPage,
estimateSize,
renderItem,
renderEmpty,
} = props;
const parentRef = useRef<HTMLDivElement>(null);
const { t } = useTranslation();
const rowVirtualizer = useVirtualizer({
count: hasNextPage ? allData.length + 1 : allData.length,
getScrollElement: () => parentRef.current,
estimateSize: () => estimateSize,
overscan: 5,
});
const virtualItems = rowVirtualizer.getVirtualItems();
useWatch([virtualItems], () => {
const lastItem = last(virtualItems);
if (!lastItem) {
return;
}
if (
lastItem.index >= allData.length - 1 &&
hasNextPage &&
!isFetchingNextPage
) {
onFetchNextPage();
}
});
return (
<div
ref={parentRef}
className="h-full w-full overflow-y-auto"
style={{
contain: 'strict',
}}
>
<div
className="relative w-full"
style={{
height: rowVirtualizer.getTotalSize(),
}}
>
{virtualItems.length === 0 &&
(renderEmpty ? renderEmpty() : <Empty />)}
<div
className="absolute left-0 top-0 w-full"
style={{
transform: `translateY(${virtualItems[0]?.start ?? 0}px)`,
}}
>
{virtualItems.map((virtualRow) => {
const isLoaderRow = virtualRow.index > allData.length - 1;
const data = allData[virtualRow.index];
return (
<div
key={virtualRow.key}
data-index={virtualRow.index}
ref={rowVirtualizer.measureElement}
>
{isLoaderRow
? hasNextPage
? t('Loading more...')
: t('Nothing more to load')
: renderItem(data)}
</div>
);
})}
</div>
</div>
</div>
);
}
);
DynamicVirtualList.displayName = 'DynamicVirtualList';

View File

@ -17,12 +17,12 @@ export const FeedEventItem: React.FC<{
return ( return (
<div <div
className={cn( className={cn(
'border-muted flex items-center rounded-lg border px-4 py-2', 'border-muted flex items-center overflow-hidden rounded-lg border px-2 py-2 sm:px-3',
className className
)} )}
> >
<div className="flex-1 gap-2"> <div className="flex-1 gap-2 overflow-hidden">
<div className="mb-2 flex items-center gap-2 text-sm"> <div className="mb-2 flex w-full items-center gap-2 overflow-hidden text-sm">
<div className="border-muted rounded-lg border p-2"> <div className="border-muted rounded-lg border p-2">
<FeedIcon source={event.source} size={24} /> <FeedIcon source={event.source} size={24} />
</div> </div>
@ -31,6 +31,7 @@ export const FeedEventItem: React.FC<{
</div> </div>
</div> </div>
<div className="flex justify-between">
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
<Badge>{event.source}</Badge> <Badge>{event.source}</Badge>
@ -40,7 +41,6 @@ export const FeedEventItem: React.FC<{
<Badge variant="outline">{tag}</Badge> <Badge variant="outline">{tag}</Badge>
))} ))}
</div> </div>
</div>
<Tooltip> <Tooltip>
<TooltipTrigger className="cursor-default self-end text-xs opacity-60"> <TooltipTrigger className="cursor-default self-end text-xs opacity-60">
@ -51,6 +51,8 @@ export const FeedEventItem: React.FC<{
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
</div> </div>
</div>
</div>
); );
}); });
FeedEventItem.displayName = 'FeedEventItem'; FeedEventItem.displayName = 'FeedEventItem';

View File

@ -15,7 +15,7 @@ import { FeedIntegration } from '@/components/feed/FeedIntegration';
import { DialogWrapper } from '@/components/DialogWrapper'; import { DialogWrapper } from '@/components/DialogWrapper';
import { useSocketSubscribeList } from '@/api/socketio'; import { useSocketSubscribeList } from '@/api/socketio';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { SimpleVirtualList } from '@/components/SimpleVirtualList'; import { DynamicVirtualList } from '@/components/DynamicVirtualList';
export const Route = createFileRoute('/feed/$channelId/')({ export const Route = createFileRoute('/feed/$channelId/')({
beforeLoad: routeAuthBeforeLoad, beforeLoad: routeAuthBeforeLoad,
@ -112,7 +112,7 @@ function PageComponent() {
} }
> >
<div className="h-full w-full overflow-hidden p-4"> <div className="h-full w-full overflow-hidden p-4">
<SimpleVirtualList <DynamicVirtualList
allData={fullEvents} allData={fullEvents}
estimateSize={100} estimateSize={100}
hasNextPage={hasNextPage} hasNextPage={hasNextPage}