Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
13 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,33 +1,70 @@
import type { ListOnItemsRenderedProps, ReactElementType } from "react-window";
import { FixedSizeList, areEqual } from "react-window";
import React, { type Ref } from "react";
import React, { type Ref, useMemo } from "react";
import type { ListChildComponentProps } from "react-window";
import type { Row as ReactTableRowType } from "react-table";
import { WIDGET_PADDING } from "constants/WidgetConstants";
import { Row } from "./Row";
import type { TableSizes } from "../Constants";
import type SimpleBar from "simplebar-react";
import { EmptyRows } from "../cellComponents/EmptyCell";
import { Text } from "@appsmith/ads";

const rowRenderer = React.memo((rowProps: ListChildComponentProps) => {
const { data, index, style } = rowProps;
const MemoizedRow = React.memo(
function RowComponent({
data,
hasMoreData,
index,
loadMore,
style,
}: ListChildComponentProps & {
loadMore?: () => void;
hasMoreData?: boolean;
}) {
if (index < data.length) {
const row = data[index];

if (index < data.length) {
const row = data[index];
return (
<Row
className="t--virtual-row"
index={index}
key={index}
row={row}
style={style}
/>
);
} else if (index === data.length && hasMoreData) {
return (
<div
onClick={loadMore}
style={{
...style,
display: "flex",
justifyContent: "flex-start",
alignItems: "center",
paddingLeft: "10px",
cursor: "pointer",
zIndex: 1000,
}}
>
<Text>Load More</Text>
</div>
);
} else {
return <EmptyRows rows={1} style={style} />;
}
},
(prevProps, nextProps) => {
if (
prevProps.loadMore !== nextProps.loadMore ||
prevProps.hasMoreData !== nextProps.hasMoreData
) {
return false;
}

return (
<Row
className="t--virtual-row"
index={index}
key={index}
row={row}
style={style}
/>
);
} else {
return <EmptyRows rows={1} style={style} />;
}
}, areEqual);
return areEqual(prevProps, nextProps);
},
);

interface BaseVirtualListProps {
height: number;
Expand All @@ -39,18 +76,38 @@ interface BaseVirtualListProps {
infiniteLoaderListRef?: React.Ref<FixedSizeList>;
itemCount: number;
pageSize: number;
loadMore?: () => void;
hasMoreData?: boolean;
}

const LOAD_MORE_BUTON_ROW = 1;

const BaseVirtualList = React.memo(function BaseVirtualList({
hasMoreData,
height,
infiniteLoaderListRef,
innerElementType,
itemCount,
loadMore,
onItemsRendered,
outerRef,
rows,
tableSizes,
}: BaseVirtualListProps) {
const rowsWithLoadMore = React.useMemo(() => {
return Object.assign(rows, { loadMore, hasMoreData });
}, [rows, loadMore, hasMoreData]);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

const rowRenderer = useMemo(() => {
return (rowProps: ListChildComponentProps) => (
<MemoizedRow
{...rowProps}
hasMoreData={hasMoreData}
loadMore={loadMore}
/>
);
}, [loadMore, hasMoreData]);

return (
<FixedSizeList
className="virtual-list simplebar-content"
Expand All @@ -60,8 +117,8 @@ const BaseVirtualList = React.memo(function BaseVirtualList({
2 * tableSizes.VERTICAL_PADDING
}
innerElementType={innerElementType}
itemCount={itemCount}
itemData={rows}
itemCount={hasMoreData ? itemCount + LOAD_MORE_BUTON_ROW : itemCount}
itemData={rowsWithLoadMore}
itemSize={tableSizes.ROW_HEIGHT}
onItemsRendered={onItemsRendered}
outerRef={outerRef}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import React, { type Ref } from "react";
import { type ReactElementType } from "react-window";
import InfiniteLoader from "react-window-infinite-loader";
import type SimpleBar from "simplebar-react";
import { LoadingIndicator } from "../../LoadingIndicator";
import { FixedInfiniteVirtualList } from "../../TableBodyCoreComponents/VirtualList";
import { useAppsmithTable } from "../../TableContext";
import { LoadingIndicator } from "../../LoadingIndicator";
import { useInfiniteVirtualization } from "./useInfiniteVirtualization";

interface InfiniteScrollBodyProps {
Expand All @@ -22,37 +21,26 @@ const InfiniteScrollBodyComponent = React.forwardRef(
tableSizes,
totalRecordsCount,
} = useAppsmithTable();
const { cachedRows, isItemLoaded, itemCount, loadMoreItems } =
useInfiniteVirtualization({
rows,
totalRecordsCount,
isLoading,
loadMore: nextPageClick,
pageSize,
});
const { cachedRows, hasMoreData, itemCount } = useInfiniteVirtualization({
rows,
totalRecordsCount,
loadMore: nextPageClick,
pageSize,
});

return (
<div className="simplebar-content-wrapper">
<InfiniteLoader
isItemLoaded={isItemLoaded}
<FixedInfiniteVirtualList
hasMoreData={hasMoreData}
height={height}
innerElementType={props.innerElementType}
itemCount={itemCount}
loadMoreItems={loadMoreItems}
minimumBatchSize={pageSize}
>
{({ onItemsRendered, ref: infiniteLoaderRef }) => (
<FixedInfiniteVirtualList
height={height}
infiniteLoaderListRef={infiniteLoaderRef}
innerElementType={props.innerElementType}
itemCount={itemCount}
onItemsRendered={onItemsRendered}
outerRef={ref}
pageSize={pageSize}
rows={cachedRows}
tableSizes={tableSizes}
/>
)}
</InfiniteLoader>
loadMore={nextPageClick}
outerRef={ref}
pageSize={pageSize}
rows={cachedRows}
tableSizes={tableSizes}
/>
{isLoading && <LoadingIndicator />}
</div>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useEffect, useMemo, useRef, useState } from "react";
import type { Row as ReactTableRowType } from "react-table";
export interface UseInfiniteVirtualizationProps {
rows: ReactTableRowType<Record<string, unknown>>[];
totalRecordsCount?: number;
isLoading: boolean;
loadMore: () => void;
pageSize: number;
}

export interface UseInfiniteVirtualizationReturn {
isItemLoaded: (index: number) => boolean;
itemCount: number;
loadMoreItems: (startIndex: number, stopIndex: number) => Promise<void>;
hasMoreData: boolean;

@rahulbarwal rahulbarwal Mar 21, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we are going via this route, does it even need this variable? I mean, can this not be calculated by page number?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, we can calculate with pageSize and rows, but putting it in the hook encapsulates that logic in a related scope. ALternatively we can remove it and have it in the InfiniteScrollBody, but we'd still have to pass it as props to BaseVirtualList. I prefer to encapsulate the logic for this inside the hook.

cachedRows: ReactTableRowType<Record<string, unknown>>[];
}

Expand All @@ -20,21 +18,14 @@ interface LoadedRowsCache {
}

export const useInfiniteVirtualization = ({
isLoading,
loadMore,
pageSize,
rows,
totalRecordsCount,
}: UseInfiniteVirtualizationProps): UseInfiniteVirtualizationReturn => {
const [loadedPages, setLoadedPages] = useState<LoadedRowsCache>({});
const lastLoadedPageRef = useRef<number>(0);
const hasMoreDataRef = useRef<boolean>(true); // Track if more data is available

const maxPages = useMemo(() => {
if (!totalRecordsCount) return Infinity;

return Math.ceil(totalRecordsCount / pageSize);
}, [totalRecordsCount, pageSize]);
const hasMoreDataRef = useRef<boolean>(true);

useEffect(() => {
if (rows.length > 0) {
Expand All @@ -52,6 +43,11 @@ export const useInfiniteVirtualization = ({
// If we got less than a full page, assume this is the last page
hasMoreDataRef.current = false;
}

// load another page in initial load if there is more data to load
if (cachedRows.length < pageSize * 2) {
loadMore();
}
} else if (rows.length === 0 && lastLoadedPageRef.current > 0) {
// If no rows are returned and we've loaded at least one page, assume end of data
hasMoreDataRef.current = false;
Expand All @@ -71,47 +67,18 @@ export const useInfiniteVirtualization = ({
return allRows;
}, [loadedPages]);

const isItemLoaded = useCallback(
(index: number) => {
const pageIndex = Math.floor(index / pageSize);

return (
pageIndex >= maxPages ||
pageIndex < lastLoadedPageRef.current ||
!hasMoreDataRef.current
);
},
[pageSize, maxPages],
);

const itemCount = useMemo(() => {
// If we know there's no more data, cap itemCount at cachedRows.length
if (!hasMoreDataRef.current) {
return cachedRows.length;
}

return totalRecordsCount || cachedRows.length;
return cachedRows.length;
}, [totalRecordsCount, cachedRows.length]);

const loadMoreItems = useCallback(
async (startIndex: number, stopIndex: number) => {
if (!isLoading && hasMoreDataRef.current) {
const targetPage = Math.floor(stopIndex / pageSize);

if (targetPage >= lastLoadedPageRef.current && targetPage < maxPages) {
loadMore();
}
}

return Promise.resolve();
},
[isLoading, loadMore, pageSize, maxPages],
);

return {
isItemLoaded,
itemCount,
loadMoreItems,
cachedRows,
hasMoreData: hasMoreDataRef.current,
};
};