Skip to content

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 without full re-fetches.

import { useTableCache } from '@requence/table'
function useTableCache<T>(
key: string,
options: UseTableCacheOptions<T>,
): TableCache<T>

key — A stable string 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. Combine dynamic parameters into the key:

const cache = useTableCache(`users-${sortField}-${sortDir}-${filter}`, {
// ...
})
OptionTypeDefaultDescription
pageSizenumberRequired. Number of rows per page. Controls how many items are fetched per request.
getItemId(item: T) => stringRequired. Extracts a unique ID from an item. Used to match items during upsert and remove.
compare(a: T, b: T) => numberRequired. 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. See fetchCount Debouncing.
PropertyTypeDescription
totalCountnumberCurrent total count. Updated by fetch results and by upsert/remove mutations.
getItem(index: number) => T | undefinedReturns the item at the given absolute index, or undefined if the page containing that index hasn’t been fetched yet.
handleRangeChange(range: { start: number; end: number }) => voidPass this directly to VirtualTable’s onRangeChange prop. Triggers page fetches for any unfetched pages within the range.
upsert(item: T) => voidInsert or update an item. See Upsert Behavior.
remove(id: string) => voidRemove an item by ID. See Remove Behavior.
reset() => voidClear all cached pages. See Reset Behavior.
loadingbooleantrue 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.

useTableCache integrates with React Suspense to provide a loading state for the initial data fetch:

  1. 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.

  2. Subsequent fetches (at least one page cached): Page fetches triggered by scrolling are non-blocking. The loading flag becomes true, and getItem() returns undefined for indices on unfetched pages — causing Body.children to return null, which renders skeleton rows.

<Suspense fallback={<TableSkeleton />}>
<UsersTable />
</Suspense>

upsert(item) handles three cases:

  1. Item exists on a cached page — The item is updated in-place at its current position. No position change, no totalCount change.

  2. 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.

  3. Item ID is genuinely new — The item is inserted at the correct sorted position using the compare function. Pages after the insertion point are invalidated (deleted from cache) because their indices have shifted.

    • If fetchCount is provided, a debounced server call updates totalCount authoritatively.
    • If fetchCount is not provided, totalCount is incremented optimistically.

remove(id) performs the following steps:

  1. The item’s ID is removed from tracking.
  2. All cached pages are searched for the item. If found, it is removed.
  3. totalCount is decremented (clamped to 0).
  4. All cached pages after the page where the item was found are invalidated, because their indices have shifted by one.

reset() discards all cached data:

  1. Any pending fetchCount timer is cleared.
  2. The internal cache iteration counter is incremented, effectively replacing the cache with a fresh, empty instance.
  3. 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).

When fetchCount is provided and a genuinely new item arrives via upsert, the cache schedules a debounced call to fetchCount() with a 150 ms delay. If another upsert arrives within that window, the timer is reset. This prevents a burst of real-time events from triggering many redundant count queries.

The authoritative count from the server replaces the optimistic totalCount, correcting any drift caused by items that were inserted on non-cached pages.

If fetchCount is not provided, the cache falls back to incrementing totalCount by 1 for each new item. This is simpler but may drift if items are frequently inserted on pages the cache hasn’t loaded.