From b24fef9c9a2f4c9d9b98174508d301f3ca9bdc89 Mon Sep 17 00:00:00 2001 From: John Kapantzakis Date: Wed, 13 May 2026 16:34:19 +0300 Subject: [PATCH 1/4] Render optional row placeholder in table body --- src/components/table/body/index.js | 87 +++++++++++++++++++++++++++++- 1 file changed, 86 insertions(+), 1 deletion(-) diff --git a/src/components/table/body/index.js b/src/components/table/body/index.js index 9e36f279..6862a7e5 100644 --- a/src/components/table/body/index.js +++ b/src/components/table/body/index.js @@ -1,4 +1,4 @@ -import React, { memo, useCallback, useRef, useEffect } from "react" +import React, { memo, useCallback, useRef, useEffect, useMemo } from "react" import { useVirtualizer, defaultRangeExtractor } from "@tanstack/react-virtual" import identity from "lodash/identity" import Flex from "@/components/templates/flex" @@ -35,6 +35,7 @@ const Body = memo( initialOffset = 0, onScroll, enableColumnReordering, + renderPlaceholder, ...rest }) => { useTableState(rerenderSelector) @@ -68,6 +69,52 @@ const Body = memo( const virtualRows = rowVirtualizer.getVirtualItems() + const placeholders = useMemo(() => { + if (!renderPlaceholder) return { before: [], after: [] } + + const range = rowVirtualizer.calculateRange() + if (!range) return { before: [], after: [] } + + const { startIndex, endIndex } = range + if (startIndex === undefined || endIndex === undefined) + return { before: [], after: [] } + + // Adjust for header: index 0 is the header row, data rows start at 1 + const firstDataIndex = 1 + const lastDataIndex = rows.length + + // "before" = data rows with index < startIndex (excluding header at 0) + const beforeStart = firstDataIndex + const beforeEnd = Math.max(beforeStart, startIndex) + + // "after" = data rows with index > endIndex + const afterStart = Math.min(lastDataIndex, endIndex + 1) + const afterEnd = lastDataIndex + 1 + + return { + before: Array.from({ length: beforeEnd - beforeStart }, (_, i) => beforeStart + i), + after: Array.from({ length: afterEnd - afterStart }, (_, i) => afterStart + i), + } + }, [renderPlaceholder, virtualRows, rows.length]) + + const getPlaceholderOffset = useCallback( + index => { + // For rows that have been measured by the virtualizer, use their known offset + const virtualItem = rowVirtualizer.getVirtualItems().find(v => v.index === index) + if (virtualItem) return virtualItem.start + + // For unmeasured rows (typically "after" rows), estimate using estimateSize + const estimateSize = rowVirtualizer.options.estimateSize + let offset = 0 + for (let i = 0; i < index; i++) { + const knownSize = rowVirtualizer.getSize(i) + offset += knownSize > 0 ? knownSize : estimateSize(i) + } + return offset + }, + [rowVirtualizer] + ) + useEffect(() => { if (!loadMore) return @@ -109,6 +156,25 @@ const Body = memo( flex: "1 0 auto", }} > + {renderPlaceholder && + placeholders.before.map(index => ( +
+ {renderPlaceholder({ + index: index - 1, + isBefore: true, + table, + })} +
+ ))} {virtualRows.map(virtualRow => { return (
) })} + {renderPlaceholder && + placeholders.after.map(index => ( +
+ {renderPlaceholder({ + index: index - 1, + isBefore: false, + table, + })} +
+ ))}
) From f8a17fa457428c171cc0a45eb03b06be53ce3baa Mon Sep 17 00:00:00 2001 From: John Kapantzakis Date: Wed, 13 May 2026 16:52:24 +0300 Subject: [PATCH 2/4] Fix row height --- src/components/table/body/index.js | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/src/components/table/body/index.js b/src/components/table/body/index.js index 6862a7e5..57783d1c 100644 --- a/src/components/table/body/index.js +++ b/src/components/table/body/index.js @@ -99,18 +99,11 @@ const Body = memo( const getPlaceholderOffset = useCallback( index => { - // For rows that have been measured by the virtualizer, use their known offset - const virtualItem = rowVirtualizer.getVirtualItems().find(v => v.index === index) - if (virtualItem) return virtualItem.start - - // For unmeasured rows (typically "after" rows), estimate using estimateSize - const estimateSize = rowVirtualizer.options.estimateSize - let offset = 0 - for (let i = 0; i < index; i++) { - const knownSize = rowVirtualizer.getSize(i) - offset += knownSize > 0 ? knownSize : estimateSize(i) - } - return offset + // measurementsCache is populated for all count items on every render + // (getTotalSize calls getMeasurements which fills the full cache). + // Entries use real sizes for measured rows and estimateSize for the rest. + const cached = rowVirtualizer.measurementsCache[index] + return cached ? cached.start : 0 }, [rowVirtualizer] ) From f7a380b180cd4dc0b69630300c8e44f694355668 Mon Sep 17 00:00:00 2001 From: John Kapantzakis Date: Thu, 14 May 2026 10:56:16 +0300 Subject: [PATCH 3/4] Fix performance --- src/components/table/body/index.js | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/src/components/table/body/index.js b/src/components/table/body/index.js index 57783d1c..93de7de1 100644 --- a/src/components/table/body/index.js +++ b/src/components/table/body/index.js @@ -68,34 +68,30 @@ const Body = memo( if (virtualRef) virtualRef.current = rowVirtualizer const virtualRows = rowVirtualizer.getVirtualItems() + // virtualRows[0] is always the sticky header (index 0); data rows start at virtualRows[1] + const firstVirtualDataIndex = virtualRows[1]?.index ?? 1 + const lastVirtualDataIndex = virtualRows[virtualRows.length - 1]?.index ?? 0 const placeholders = useMemo(() => { if (!renderPlaceholder) return { before: [], after: [] } - const range = rowVirtualizer.calculateRange() - if (!range) return { before: [], after: [] } - - const { startIndex, endIndex } = range - if (startIndex === undefined || endIndex === undefined) - return { before: [], after: [] } - - // Adjust for header: index 0 is the header row, data rows start at 1 + const N = overscan || 15 const firstDataIndex = 1 const lastDataIndex = rows.length - // "before" = data rows with index < startIndex (excluding header at 0) - const beforeStart = firstDataIndex - const beforeEnd = Math.max(beforeStart, startIndex) + // "before" = up to N data rows immediately before the virtual window (outside overscan) + const beforeEnd = firstVirtualDataIndex + const beforeStart = Math.max(firstDataIndex, beforeEnd - N) - // "after" = data rows with index > endIndex - const afterStart = Math.min(lastDataIndex, endIndex + 1) - const afterEnd = lastDataIndex + 1 + // "after" = up to N data rows immediately after the virtual window (outside overscan) + const afterStart = lastVirtualDataIndex + 1 + const afterEnd = Math.min(lastDataIndex + 1, afterStart + N) return { before: Array.from({ length: beforeEnd - beforeStart }, (_, i) => beforeStart + i), after: Array.from({ length: afterEnd - afterStart }, (_, i) => afterStart + i), } - }, [renderPlaceholder, virtualRows, rows.length]) + }, [renderPlaceholder, firstVirtualDataIndex, lastVirtualDataIndex, rows.length, overscan]) const getPlaceholderOffset = useCallback( index => { From 0d28ab96c59a41a6328715e7cdaf5fe93fd29c93 Mon Sep 17 00:00:00 2001 From: John Kapantzakis Date: Thu, 14 May 2026 12:01:03 +0300 Subject: [PATCH 4/4] Change comment wording --- src/components/table/body/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/table/body/index.js b/src/components/table/body/index.js index 93de7de1..2586c25f 100644 --- a/src/components/table/body/index.js +++ b/src/components/table/body/index.js @@ -68,7 +68,7 @@ const Body = memo( if (virtualRef) virtualRef.current = rowVirtualizer const virtualRows = rowVirtualizer.getVirtualItems() - // virtualRows[0] is always the sticky header (index 0); data rows start at virtualRows[1] + // index 0 is reserved for the sticky header (see count = rows + 1 and rangeExtractor above) const firstVirtualDataIndex = virtualRows[1]?.index ?? 1 const lastVirtualDataIndex = virtualRows[virtualRows.length - 1]?.index ?? 0