perf(ui): a next-page fetch joins the one in flight instead of sending the cursor again

react-query's fetchNextPage cancels a next page still loading and sends it again by default.
Our requests ignore the abort signal, so the server ran both: the feed's read-ahead effect
re-runs on scroll frames before isFetchingNextPage reaches it, and on prod every feed page
went out 2-3 times (a 0.8 s title sort queued to 2.3 s). Every call now passes NEXT_PAGE
({cancelRefetch: false}), lint-enforced, with a contract test.
This commit is contained in:
2026-10-02 20:46:30 +02:00
parent b4ef07d28e
commit 540822d2b0
9 changed files with 98 additions and 7 deletions
+4 -2
View File
@@ -10,6 +10,7 @@ import {
import { useVirtualizer } from "@tanstack/react-virtual";
import { useListNav } from "../../lib/listNav";
import { scrollParent } from "../../lib/listPaging";
import { NEXT_PAGE } from "../../lib/nextPage";
import { GRID_GAP, columnsFor, itemWidthFor } from "./gridColumns";
import { shouldPrefetchNext } from "./readAhead";
@@ -66,7 +67,8 @@ export interface VirtualGridProps<T> {
/** Infinite scroll — omit all three for a finite client-side list. */
hasNextPage?: boolean;
isFetchingNextPage?: boolean;
fetchNextPage?: () => void;
/** Called with `NEXT_PAGE`, so a read-ahead that fires again joins the page already loading. */
fetchNextPage?: (options: typeof NEXT_PAGE) => unknown;
}
export default function VirtualGrid<T>({
@@ -167,7 +169,7 @@ export default function VirtualGrid<T>({
const last = virtualRows[virtualRows.length - 1];
if (!last) return;
if (shouldPrefetchNext(last.index, rows.length, virtualRows.length)) {
fetchNextPage();
fetchNextPage(NEXT_PAGE);
}
}, [virtualRows, rows.length, hasNextPage, isFetchingNextPage, fetchNextPage]);