Skip to content

Commit 6308914

Browse files
committed
fix: virtualized rows
1 parent eb21487 commit 6308914

File tree

4 files changed

+112
-81
lines changed

4 files changed

+112
-81
lines changed

src/components/PaginatedTable/PaginatedTable.tsx

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export interface PaginatedTableProps<T, F> {
3636
keepCache?: boolean;
3737
}
3838

39-
const DEFAULT_PAGINATION_LIMIT = 20;
39+
const DEFAULT_PAGINATION_LIMIT = 50;
4040

4141
export const PaginatedTable = <T, F>({
4242
limit: chunkSize = DEFAULT_PAGINATION_LIMIT,
@@ -63,7 +63,7 @@ export const PaginatedTable = <T, F>({
6363

6464
const tableRef = React.useRef<HTMLDivElement>(null);
6565

66-
const activeChunks = useScrollBasedChunks({
66+
const {visibleRowRange, totalItems} = useScrollBasedChunks({
6767
scrollContainerRef,
6868
tableRef,
6969
totalItems: foundEntities,
@@ -78,15 +78,6 @@ export const PaginatedTable = <T, F>({
7878
setFilters(rawFilters);
7979
}, [rawFilters]);
8080

81-
const lastChunkSize = React.useMemo(() => {
82-
// If foundEntities = 0, there will only first chunk
83-
// Display it with 1 row, to display empty data message
84-
if (!foundEntities) {
85-
return 1;
86-
}
87-
return foundEntities % chunkSize || chunkSize;
88-
}, [foundEntities, chunkSize]);
89-
9081
const handleDataFetched = React.useCallback(
9182
(data?: PaginatedTableData<T>) => {
9283
if (data) {
@@ -121,9 +112,9 @@ export const PaginatedTable = <T, F>({
121112
}, [initialEntitiesCount, setTotalEntities, setFoundEntities, setIsInitialLoad]);
122113

123114
const {renderChunks} = useVirtualizedTbodies({
124-
activeChunks,
115+
visibleRowRange,
116+
totalItems,
125117
chunkSize,
126-
lastChunkSize,
127118
rowHeight,
128119
columns,
129120
fetchData,

src/components/PaginatedTable/TableChunk.tsx

Lines changed: 43 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ interface TableChunkProps<T, F> {
3030
filters?: F;
3131
sortParams?: SortParams;
3232
tableName: string;
33+
startRow: number;
34+
endRow: number;
3335

3436
fetchData: FetchData<T, F>;
3537
getRowClassName?: GetRowClassName<T>;
@@ -56,6 +58,8 @@ export const TableChunk = typedMemo(function TableChunk<T, F>({
5658
renderEmptyDataMessage,
5759
onDataFetched,
5860
keepCache,
61+
startRow,
62+
endRow,
5963
}: TableChunkProps<T, F>) {
6064
const [isTimeoutActive, setIsTimeoutActive] = React.useState(true);
6165
const [autoRefreshInterval] = useAutoRefreshInterval();
@@ -111,41 +115,59 @@ export const TableChunk = typedMemo(function TableChunk<T, F>({
111115
if (!currentData) {
112116
if (error) {
113117
const errorData = error as IResponseError;
114-
return (
115-
<EmptyTableRow columns={columns}>
118+
return [
119+
<EmptyTableRow key="empty" columns={columns}>
116120
{renderErrorMessage ? (
117121
renderErrorMessage(errorData)
118122
) : (
119123
<ResponseError error={errorData} />
120124
)}
121-
</EmptyTableRow>
122-
);
125+
</EmptyTableRow>,
126+
];
123127
} else {
124-
return getArray(dataLength).map((value) => (
125-
<LoadingTableRow key={value} columns={columns} height={rowHeight} />
126-
));
128+
return getArray(dataLength)
129+
.map((value, index) => {
130+
const globalRowIndex = id * chunkSize + index;
131+
132+
if (globalRowIndex < startRow || globalRowIndex > endRow) {
133+
return null;
134+
}
135+
136+
return <LoadingTableRow key={value} columns={columns} height={rowHeight} />;
137+
})
138+
.filter(Boolean);
127139
}
128140
}
129141

130142
// Data is loaded, but there are no entities in the chunk
131143
if (!currentData.data?.length) {
132-
return (
133-
<EmptyTableRow columns={columns}>
144+
return [
145+
<EmptyTableRow key="empty" columns={columns}>
134146
{renderEmptyDataMessage ? renderEmptyDataMessage() : i18n('empty')}
135-
</EmptyTableRow>
136-
);
147+
</EmptyTableRow>,
148+
];
137149
}
138150

139-
return currentData.data.map((rowData, index) => (
140-
<TableRow
141-
key={index}
142-
row={rowData as T}
143-
columns={columns}
144-
height={rowHeight}
145-
getRowClassName={getRowClassName}
146-
/>
147-
));
151+
return currentData.data
152+
.map((rowData, index) => {
153+
const globalRowIndex = id * chunkSize + index;
154+
155+
if (globalRowIndex < startRow || globalRowIndex > endRow) {
156+
return null;
157+
}
158+
159+
return (
160+
<TableRow
161+
key={index}
162+
row={rowData as T}
163+
columns={columns}
164+
height={rowHeight}
165+
getRowClassName={getRowClassName}
166+
/>
167+
);
168+
})
169+
.filter(Boolean);
148170
};
149171

150-
return <React.Fragment>{renderContent()}</React.Fragment>;
172+
return renderContent();
151173
});

src/components/PaginatedTable/useScrollBasedChunks.ts

Lines changed: 33 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ interface UseScrollBasedChunksProps {
1111
overscanCount?: number;
1212
}
1313

14-
const DEFAULT_OVERSCAN_COUNT = 2;
14+
const DEFAULT_OVERSCAN_COUNT = 15;
1515

1616
export const useScrollBasedChunks = ({
1717
scrollContainerRef,
@@ -20,15 +20,18 @@ export const useScrollBasedChunks = ({
2020
rowHeight,
2121
chunkSize,
2222
overscanCount = DEFAULT_OVERSCAN_COUNT,
23-
}: UseScrollBasedChunksProps): boolean[] => {
23+
}: UseScrollBasedChunksProps): {
24+
visibleRowRange: {start: number; end: number};
25+
totalItems: number;
26+
} => {
2427
const chunksCount = React.useMemo(
2528
() => Math.ceil(totalItems / chunkSize),
2629
[chunkSize, totalItems],
2730
);
2831

29-
const [startChunk, setStartChunk] = React.useState(0);
30-
const [endChunk, setEndChunk] = React.useState(
31-
Math.min(overscanCount, Math.max(chunksCount - 1, 0)),
32+
const [startRow, setStartRow] = React.useState(0);
33+
const [endRow, setEndRow] = React.useState(
34+
Math.min(overscanCount, Math.max(totalItems - 1, 0)),
3235
);
3336

3437
const calculateVisibleRange = React.useCallback(() => {
@@ -43,19 +46,30 @@ export const useScrollBasedChunks = ({
4346
const visibleStart = Math.max(containerScroll - tableOffset, 0);
4447
const visibleEnd = visibleStart + container.clientHeight;
4548

46-
const start = Math.max(Math.floor(visibleStart / rowHeight / chunkSize) - overscanCount, 0);
47-
const end = Math.min(
48-
Math.floor(visibleEnd / rowHeight / chunkSize) + overscanCount,
49-
Math.max(chunksCount - 1, 0),
50-
);
51-
return {start, end};
52-
}, [scrollContainerRef, tableRef, rowHeight, chunkSize, overscanCount, chunksCount]);
49+
// Calculate row range first
50+
const rowStart = Math.max(Math.floor(visibleStart / rowHeight) - overscanCount, 0);
51+
const rowEnd = Math.min(Math.floor(visibleEnd / rowHeight) + overscanCount, totalItems - 1);
52+
53+
// Calculate chunk range from row range
54+
const start = Math.max(Math.floor(rowStart / chunkSize), 0);
55+
const end = Math.min(Math.floor(rowEnd / chunkSize), Math.max(chunksCount - 1, 0));
56+
57+
return {start, end, rowStart, rowEnd};
58+
}, [
59+
scrollContainerRef,
60+
tableRef,
61+
rowHeight,
62+
chunkSize,
63+
overscanCount,
64+
chunksCount,
65+
totalItems,
66+
]);
5367

5468
const updateVisibleChunks = React.useCallback(() => {
5569
const newRange = calculateVisibleRange();
5670
if (newRange) {
57-
setStartChunk(newRange.start);
58-
setEndChunk(newRange.end);
71+
setStartRow(newRange.rowStart);
72+
setEndRow(newRange.rowEnd);
5973
}
6074
}, [calculateVisibleRange]);
6175

@@ -94,11 +108,9 @@ export const useScrollBasedChunks = ({
94108
}, [handleScroll, scrollContainerRef]);
95109

96110
return React.useMemo(() => {
97-
// boolean array that represents active chunks
98-
const activeChunks = Array(chunksCount).fill(false);
99-
for (let i = startChunk; i <= endChunk; i++) {
100-
activeChunks[i] = true;
101-
}
102-
return activeChunks;
103-
}, [chunksCount, startChunk, endChunk]);
111+
return {
112+
visibleRowRange: {start: startRow, end: endRow},
113+
totalItems,
114+
};
115+
}, [startRow, endRow, totalItems]);
104116
};

src/components/PaginatedTable/useVirtualizedTbodies.tsx

Lines changed: 32 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ import type {
1212
} from './types';
1313

1414
interface UseVirtualizedTbodiesProps<T, F> {
15-
activeChunks: boolean[];
15+
visibleRowRange: {start: number; end: number};
16+
totalItems: number;
1617
chunkSize: number;
17-
lastChunkSize: number;
1818
rowHeight: number;
1919
columns: Column<T>[];
2020
fetchData: FetchData<T, F>;
@@ -29,9 +29,9 @@ interface UseVirtualizedTbodiesProps<T, F> {
2929
}
3030

3131
export const useVirtualizedTbodies = <T, F>({
32-
activeChunks,
32+
visibleRowRange,
33+
totalItems,
3334
chunkSize,
34-
lastChunkSize,
3535
rowHeight,
3636
columns,
3737
fetchData,
@@ -44,35 +44,36 @@ export const useVirtualizedTbodies = <T, F>({
4444
onDataFetched,
4545
keepCache = true,
4646
}: UseVirtualizedTbodiesProps<T, F>) => {
47+
const startRow = visibleRowRange.start;
48+
const endRow = visibleRowRange.end;
49+
4750
const renderChunks = React.useCallback(() => {
4851
const chunks: React.ReactElement[] = [];
4952

50-
// Count empty start chunks
51-
let startEmptyCount = 0;
52-
while (startEmptyCount < activeChunks.length && !activeChunks[startEmptyCount]) {
53-
startEmptyCount++;
54-
}
53+
// Calculate which chunks contain visible rows
54+
const totalChunks = Math.ceil(totalItems / chunkSize);
55+
const startChunk = Math.max(0, Math.floor(startRow / chunkSize));
56+
const endChunk = Math.min(totalChunks - 1, Math.floor(endRow / chunkSize));
5557

56-
// Push start spacer if needed
57-
if (startEmptyCount > 0) {
58+
// Push start spacer for rows before visible range
59+
const startSpacerHeight = startRow * rowHeight;
60+
if (startSpacerHeight > 0) {
5861
chunks.push(
5962
<tbody
6063
key="spacer-start"
6164
style={{
62-
height: `${startEmptyCount * chunkSize * rowHeight}px`,
65+
height: `${startSpacerHeight}px`,
6366
display: 'block',
6467
}}
6568
/>,
6669
);
6770
}
6871

69-
// Collect active chunks and calculate total height
72+
// Collect active chunks and calculate height for visible rows only
7073
const activeChunkElements: React.ReactElement[] = [];
71-
let totalActiveHeight = 0;
7274

73-
for (let i = startEmptyCount; i < activeChunks.length && activeChunks[i]; i++) {
74-
const chunkRowCount = i === activeChunks.length - 1 ? lastChunkSize : chunkSize;
75-
totalActiveHeight += chunkRowCount * rowHeight;
75+
for (let i = startChunk; i <= endChunk; i++) {
76+
const chunkRowCount = i === totalChunks - 1 ? totalItems - i * chunkSize : chunkSize;
7677

7778
activeChunkElements.push(
7879
<TableChunk<T, F>
@@ -91,13 +92,18 @@ export const useVirtualizedTbodies = <T, F>({
9192
renderEmptyDataMessage={renderEmptyDataMessage}
9293
onDataFetched={onDataFetched}
9394
keepCache={keepCache}
95+
startRow={startRow}
96+
endRow={endRow}
9497
/>,
9598
);
96-
startEmptyCount = i + 1;
9799
}
98100

99-
// Wrap active chunks in a single tbody with calculated height
101+
// Wrap active chunks in a single tbody
100102
if (activeChunkElements.length > 0) {
103+
// Calculate height based on visible rows only
104+
const visibleRowCount = endRow - startRow + 1;
105+
const totalActiveHeight = visibleRowCount * rowHeight;
106+
101107
chunks.push(
102108
<tbody
103109
key="active-chunks"
@@ -111,16 +117,15 @@ export const useVirtualizedTbodies = <T, F>({
111117
);
112118
}
113119

114-
// Count empty end chunks
115-
const endEmptyCount = activeChunks.length - startEmptyCount;
120+
// Add end spacer for rows after visible range
121+
const endSpacerHeight = Math.max(0, (totalItems - startRow - 1) * rowHeight);
116122

117-
// Push end spacer if needed
118-
if (endEmptyCount > 0) {
123+
if (endSpacerHeight > 0) {
119124
chunks.push(
120125
<tbody
121126
key="spacer-end"
122127
style={{
123-
height: `${endEmptyCount * chunkSize * rowHeight}px`,
128+
height: `${endSpacerHeight}px`,
124129
display: 'block',
125130
}}
126131
/>,
@@ -129,9 +134,10 @@ export const useVirtualizedTbodies = <T, F>({
129134

130135
return chunks;
131136
}, [
132-
activeChunks,
137+
startRow,
138+
endRow,
139+
totalItems,
133140
chunkSize,
134-
lastChunkSize,
135141
rowHeight,
136142
columns,
137143
fetchData,

0 commit comments

Comments
 (0)