useTableCache
useTableCache manages paginated, sorted data for VirtualTable. It fetches pages on demand as the user scrolls, integrates with React Suspense for the initial load, and provides upsert / remove methods for real-time updates with surgical cache modifications and automatic scroll correction.
Import
Section titled “Import”import { useTableCache } from '@requence/table'Signature
Section titled “Signature”function useTableCache<T>( key: CacheKey, options: UseTableCacheOptions<T>,): TableCache<T>key — A stable CacheKey (string | number | (string | number)[]) that identifies this cache. When the key changes (e.g. because a filter or sort order changed), the cache is discarded and the next render will re-suspend.
You can pass a single string or number:
const cache = useTableCache('users', { /* ... */ })Or combine multiple dynamic parameters into an array key — the elements are joined with '-' internally:
const cache = useTableCache(['users', sortField, sortDir, filter], { // ...})This is equivalent to building the string yourself, but avoids manual concatenation and makes each segment explicit:
// Same effect as the array key aboveconst cache = useTableCache(`users-${sortField}-${sortDir}-${filter}`, { // ...})UseTableCacheOptions
Section titled “UseTableCacheOptions”| Option | Type | Default | Description |
|---|---|---|---|
pageSize | number | — | Required. Number of rows per page. Controls how many items are fetched per request. |
rowHeight | number | — | Required. Fixed height of each row in pixels. Used to calculate scroll corrections when items are added or removed above the viewport. |
rowGap | number | 0 | Gap between rows in pixels. Must match the rowGap passed to VirtualTable. Used to calculate scroll corrections — each row occupies rowHeight + rowGap pixels of scroll space. |
getItemId | (item: T) => string | — | Required. Extracts a unique ID from an item. Used to match items during upsert and remove. |
compare | (a: T, b: T) => number | — | Required. Comparator for sort order. Return negative if a comes before b, positive if after, 0 if equal. Used by upsert() to binary-search the correct insertion position. |
fetchItems | (offset: number, limit: number) => Promise<{ items: T[]; total: number }> | — | Required. Fetches a page of data from the server. Must return the items for the requested range and the total count. See Suspense Behavior. |
fetchCount | () => Promise<number> | — | Optional. Fetches just the total count. Called (debounced, 150 ms) when an upsert arrives for an unknown ID, and on every remove. See fetchCount Debouncing. |
TableCache (Return Value)
Section titled “TableCache (Return Value)”| Property | Type | Description |
|---|---|---|
ref | React.RefObject<VirtualTableHandle | null> | Ref to pass to VirtualTable for scroll correction via imperative handle. |
rowHeight | number | The rowHeight value passed in options. Included so {...cache} wires it up on VirtualTable automatically. |
rowGap | number | The rowGap value passed in options (or 0). Included so {...cache} wires it up on VirtualTable automatically. |
totalCount | number | Current total count. Updated by fetch results and by upsert/remove mutations. |
getItem | (index: number) => T | undefined | Returns the item at the given absolute index, or undefined if the page containing that index hasn’t been fetched yet. |
onRangeChange | (range: { start: number; end: number }) => void | Pass this directly to VirtualTable’s onRangeChange prop. Triggers page fetches for any unfetched pages within the range. |
upsert | (item: T) => void | Insert or update an item with surgical cache modification. See Upsert Behavior. |
remove | (id: string) => void | Remove an item by ID with surgical cache adjustment and immediate scroll correction. See Remove Behavior. |
reset | () => void | Clear all cached pages. See Reset Behavior. |
loading | boolean | true when a scroll-triggered page fetch is in-flight. false during the initial Suspense-suspended fetch. Use this to show a loading indicator in the header or footer. |
getTotalCount | () => number | Stable callback that returns the current totalCount. Useful in event handlers or effects that need the latest count without stale closure issues. |
Suspense Behavior
Section titled “Suspense Behavior”useTableCache integrates with React Suspense to provide a loading state for the initial data fetch:
-
First fetch (no cached pages): The promise returned by
fetchItems(0, pageSize)is thrown, causing the component to suspend. Wrap the table in a<Suspense>boundary to show a fallback. -
Subsequent fetches (at least one page cached): Page fetches triggered by scrolling are non-blocking. The
loadingflag becomestrue, andgetItem()returnsundefinedfor indices on unfetched pages — causingBody.childrento returnnull, which renders skeleton rows.
<Suspense fallback={<TableSkeleton />}> <UsersTable /></Suspense>Upsert Behavior
Section titled “Upsert Behavior”upsert(item) handles six cases:
-
Item exists on a cached page — The item is updated in-place at its current position. No position change, no
totalCountchange. -
Item ID is known but on a non-cached page — The item is skipped. The cache has seen this ID in a previous fetch but the page has since been evicted or was never loaded. No action is taken because the server already has the correct data for that page.
-
Unknown ID, sorts within a cached page — The item is inserted at the correct sorted position using
compare. Subsequent contiguous pages are surgically shifted: the last item is popped from each page and unshifted onto the next, preserving page boundaries without re-fetching.totalCountis incremented by 1 immediately. IffetchCountis provided, a debounced server call corrects any accumulated drift. -
Unknown ID, sorts before first cached item — The item is not inserted into the cache.
pendingAboveCountis incremented (the item is above the viewport).fetchCountis queried. Scroll correction is applied when the count response arrives. -
Unknown ID, sorts after last cached item — The item is not inserted.
fetchCountis queried to update the total. No scroll correction is needed. -
Unknown ID, sorts between non-contiguous cached pages — The item falls in a gap and is not inserted.
fetchCountis queried. If the item sorts before the visible range,pendingAboveCountis incremented for later scroll correction.
Remove Behavior
Section titled “Remove Behavior”remove(id) performs surgical cache adjustment:
- The item’s ID is marked as removed in the
knownIdstracking map. It stays there as a tombstone, so a repeatedremove(id)returns immediately —upsertclears it if the item ever re-enters the result set. - If the item is on a cached page, it is spliced out and subsequent contiguous pages are surgically pulled: the first item from each next page fills the gap. The last contiguous page shrinks by one.
totalCountis adjusted according to what the cache knows about the ID — see below.- If the item was above the viewport — either on a cached page before the visible range, or its last-known page index (from
knownIds) is before the visible range — an immediate scroll correction of-(rowHeight + rowGap)is applied to prevent layout shift.
totalCount
Section titled “totalCount”| What the cache knows about the ID | Effect on totalCount |
|---|---|
Seen in a fetched page, or added by a previous upsert | Decremented by 1 (clamped to 0), then reconciled by a debounced fetchCount call. |
| Never seen | fetchCount is queried and its answer replaces the count. Without fetchCount, decremented by 1. |
| Already removed | Unchanged — the call is a no-op. |
Reset Behavior
Section titled “Reset Behavior”reset() discards all cached data and resets the viewport:
- Any pending
fetchCounttimer is cleared. - The scroll position is reset to the top via
scrollTo(0). - The internal cache iteration counter is incremented, effectively replacing the cache with a fresh, empty instance.
- The next render will re-suspend because no pages exist — the Suspense fallback is shown again while the first page is re-fetched.
Use reset() when the underlying data has changed in a way that can’t be expressed through upsert/remove (e.g. a bulk import, a filter change handled outside the key, or a manual refresh button).
fetchCount Debouncing
Section titled “fetchCount Debouncing”When fetchCount is provided, the cache schedules a debounced call to fetchCount() with a 150 ms delay — when an unknown item arrives via upsert, and on every remove (to settle an unknown ID, or to correct drift after a known one). If another mutation arrives within that window, the timer is reset. This prevents a burst of real-time events from triggering many redundant count queries.
When the count response arrives, the cache:
- Replaces the optimistic
totalCountwith the authoritative server count. - Computes
aboveAdjustment = Math.min(pendingAboveCount, Math.max(0, countDelta))— how many of the pending above-viewport items actually changed the count. - Resets
pendingAboveCountto 0. - Calls
scrollBy(aboveAdjustment * (rowHeight + rowGap))on the VirtualTable handle to keep visible content stable.
If fetchCount is not provided, the cache falls back to adjusting totalCount by 1 per new or removed item. This is simpler but may drift if items are frequently inserted on pages the cache hasn’t loaded, or if removals arrive for IDs it has never seen.