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
+3 -2
View File
@@ -13,6 +13,7 @@ import { api, type LogFacet, type LogQuery, type ServerLogRow } from "../lib/api
import { copyText } from "../lib/clipboard";
import { formatDateTimeSec } from "../lib/format";
import { nextNavId, notifyNav, rememberNavPosition } from "../lib/history";
import { NEXT_PAGE } from "../lib/nextPage";
import { LEVEL_GROUPS, type LevelGroup } from "../lib/log";
import {
DEFAULT_LOGS_STATE,
@@ -156,7 +157,7 @@ export default function AdminLogs() {
? "countsFailed"
: null;
const retryFailed = () => {
if (logs.isFetchNextPageError) void logs.fetchNextPage();
if (logs.isFetchNextPageError) void logs.fetchNextPage(NEXT_PAGE);
else if (logs.isError) void logs.refetch();
if (facets.isError) void facets.refetch();
};
@@ -190,7 +191,7 @@ export default function AdminLogs() {
const loadOlder = () => {
setLive(false);
void logs.fetchNextPage();
void logs.fetchNextPage(NEXT_PAGE);
};
const copy = async () => {
+2 -1
View File
@@ -7,6 +7,7 @@ import {
useQueryClient,
} from "@tanstack/react-query";
import { qk } from "../lib/queryKeys";
import { NEXT_PAGE } from "../lib/nextPage";
import {
ArrowLeft,
Columns2,
@@ -556,7 +557,7 @@ export default function Feed({
<div className="text-center pt-4">
<Button
variant="outline"
onClick={() => ytQuery.fetchNextPage()}
onClick={() => void ytQuery.fetchNextPage(NEXT_PAGE)}
loading={ytQuery.isFetchingNextPage}
>
{ytQuery.isFetchingNextPage
+3 -1
View File
@@ -9,6 +9,7 @@ import {
} from "@tanstack/react-query";
import { User, X } from "lucide-react";
import { qk } from "../lib/queryKeys";
import { NEXT_PAGE } from "../lib/nextPage";
import { healPlexField } from "../lib/plexSearch";
import { healPlexSort, plexSortOptions, PLEX_SORT_FALLBACK } from "../lib/plexSort";
import type { SortState } from "../lib/columnSort";
@@ -321,7 +322,8 @@ export default function PlexBrowse() {
if (!sentinelEl) return;
const io = new IntersectionObserver(
(entries) => {
if (entries[0]?.isIntersecting && hasNextPage && !isFetchingNextPage) fetchNextPage();
if (entries[0]?.isIntersecting && hasNextPage && !isFetchingNextPage)
void fetchNextPage(NEXT_PAGE);
},
{ rootMargin: "600px" },
);
+2 -1
View File
@@ -1,5 +1,6 @@
import type { Video, VideoStatus } from "../lib/api";
import { FEED_VIEW_SPEC, type FeedView } from "../lib/feedView";
import type { NEXT_PAGE } from "../lib/nextPage";
import VideoCard from "./VideoCard";
import { VideoMenu } from "./VideoMenu";
import { useContextMenu } from "./ui/useContextMenu";
@@ -29,7 +30,7 @@ export interface VirtualFeedProps {
onOpen: (v: Video, startAt?: number | null) => void;
hasNextPage: boolean;
isFetchingNextPage: boolean;
fetchNextPage: () => void;
fetchNextPage: (options: typeof NEXT_PAGE) => unknown;
}
// The feed's videos on the shared VirtualGrid engine — this wrapper only maps a FeedView to the
+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]);