fix(feed): stop search-box flicker (keepPreviousData + debounced query)

Typing in the search box changed the feed/count/facets query keys on every keystroke, so
each query dropped to its loading state and blanked the content — the whole feed area
flickered. Debounce the search term feeding the queries (the input still updates instantly)
so they only re-run after a pause, and keep previous results on screen during a refetch via
placeholderData: keepPreviousData, so the feed and tag counts update in place without blanking.
This commit is contained in:
npeter83 2026-06-29 02:19:18 +02:00
parent 8b19faaed1
commit 74cf31b58d
3 changed files with 35 additions and 8 deletions

View file

@ -1,10 +1,11 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { useInfiniteQuery, useQuery, useQueryClient } from "@tanstack/react-query";
import { keepPreviousData, useInfiniteQuery, useQuery, useQueryClient } from "@tanstack/react-query";
import { ArrowDown, ArrowUp, ArrowLeft, RefreshCw, Youtube } from "lucide-react";
import { api, HttpError, type FeedFilters, type Video } from "../lib/api";
import i18n from "../i18n";
import { notify, resolveVideo } from "../lib/notifications";
import { useDebounced } from "../lib/useDebounced";
import VirtualFeed from "./VirtualFeed";
import PlayerModal from "./PlayerModal";
@ -100,12 +101,19 @@ export default function Feed({
const ytActive = !!ytSearch;
// Debounce only the search term feeding the queries: the input still updates instantly (it
// owns filters.q), but the feed/count keys settle after a pause, so typing doesn't refetch —
// and re-fetched pages keep the previous results on screen, so the feed never blanks/flickers.
const debouncedQ = useDebounced(filters.q, 300);
const queryFilters: FeedFilters = { ...filters, q: debouncedQ };
const query = useInfiniteQuery({
queryKey: ["feed", filters],
queryFn: ({ pageParam }) => api.feed(filters, pageParam as string | null, PAGE),
queryKey: ["feed", queryFilters],
queryFn: ({ pageParam }) => api.feed(queryFilters, pageParam as string | null, PAGE),
initialPageParam: null as string | null,
getNextPageParam: (last) => last.next_cursor ?? undefined,
enabled: !ytActive, // don't load the normal feed while showing live YouTube results
placeholderData: keepPreviousData,
});
// Live YouTube search results. Each page spends 100 quota units, so we never auto-paginate
@ -135,10 +143,11 @@ export default function Feed({
}, [query.dataUpdatedAt]);
const countQuery = useQuery({
queryKey: ["feed-count", filters],
queryFn: () => api.feedCount(filters),
queryKey: ["feed-count", queryFilters],
queryFn: () => api.feedCount(queryFilters),
staleTime: 30_000,
enabled: !ytActive,
placeholderData: keepPreviousData,
});
const { hasNextPage, isFetchingNextPage, fetchNextPage } = query;