From 399397117a348e954a2cfabdb5c3562c2b2ec6e3 Mon Sep 17 00:00:00 2001 From: "shakespeare.diy" Date: Wed, 18 Feb 2026 10:43:04 -0600 Subject: [PATCH] =?UTF-8?q?fix:=20move=20prefetch=20out=20of=20queryFn=20i?= =?UTF-8?q?nto=20useEffect=20=E2=80=94=20zero=20pagination=20blocking?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The queryFn now only fetches feed events and returns them immediately. Author and stats prefetching moves to a useEffect in Feed.tsx that fires after each render where feedItems changes. The 400px early sentinel means the next page starts fetching while the user is still reading. The useEffect prefetch runs concurrently — by the time the user scrolls to new cards, authors are likely already cached. Cards that miss the window fall back to individual useAuthor queries. Co-authored-by: shakespeare.diy --- src/components/Feed.tsx | 13 ++++++++++++- src/hooks/useFeed.ts | 25 ++++++++----------------- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/src/components/Feed.tsx b/src/components/Feed.tsx index ebba592c..7b3446cc 100644 --- a/src/components/Feed.tsx +++ b/src/components/Feed.tsx @@ -9,7 +9,8 @@ import { Button } from '@/components/ui/button'; import { Loader2 } from 'lucide-react'; import LoginDialog from '@/components/auth/LoginDialog'; import SignupDialog from '@/components/auth/SignupDialog'; -import { useFeed } from '@/hooks/useFeed'; +import { useNostr } from '@nostrify/react'; +import { useFeed, prefetchFeedData } from '@/hooks/useFeed'; import { useCurrentUser } from '@/hooks/useCurrentUser'; import { cn } from '@/lib/utils'; import type { FeedItem } from '@/hooks/useFeed'; @@ -27,6 +28,7 @@ export function Feed() { } }, [user]); + const { nostr } = useNostr(); const queryClient = useQueryClient(); const { @@ -67,6 +69,15 @@ export function Feed() { return items; }, [data?.pages]); + // After each new page renders, prefetch authors and stats in the background. + // Runs after render so it never blocks pagination — the 400px early sentinel + // gives enough lead time for profiles to arrive before the user scrolls to them. + useEffect(() => { + if (feedItems.length > 0) { + prefetchFeedData(feedItems, nostr, queryClient); + } + }, [feedItems, nostr, queryClient]); + const handleLogin = () => { setLoginDialogOpen(false); setSignupDialogOpen(false); diff --git a/src/hooks/useFeed.ts b/src/hooks/useFeed.ts index cb735630..05d78e58 100644 --- a/src/hooks/useFeed.ts +++ b/src/hooks/useFeed.ts @@ -41,15 +41,15 @@ function parseRepostContent(repost: NostrEvent): NostrEvent | undefined { } /** - * Fire-and-forget: seed ['author', pubkey] and ['event-stats', id] caches - * for a page of feed items so that NoteCard's per-card hooks resolve from - * cache rather than firing individual relay queries. + * Prefetch author profiles and interaction stats for a set of feed items, + * seeding the TanStack Query cache so per-card hooks resolve without opening + * individual relay subscriptions. * - * Runs in the background — does NOT block the queryFn from returning items. - * Includes post authors, repost authors, and mentioned p-tag pubkeys so that - * @mentions and reply-to lines also resolve without individual round trips. + * Called from a useEffect in Feed.tsx — runs after render, never blocks + * the queryFn. Authors use a 1500ms timeout (partial results are fine); + * stats are fully fire-and-forget. */ -async function prefetchPageData( +export function prefetchFeedData( items: FeedItem[], nostr: ReturnType['nostr'], queryClient: ReturnType, @@ -71,11 +71,8 @@ async function prefetchPageData( .map((item) => item.event.id) .filter((id) => queryClient.getQueryData(['event-stats', id]) === undefined); - // Await author prefetch with a short deadline — whatever profiles arrive - // in time get cached; cards whose authors miss the window fall back to - // individual useAuthor queries (acceptable for a minority of cards). if (pubkeysToFetch.length > 0) { - await nostr.query( + nostr.query( [{ kinds: [0], authors: pubkeysToFetch }], { signal: AbortSignal.timeout(1500) }, ).then((profileEvents) => { @@ -87,7 +84,6 @@ async function prefetchPageData( }).catch(() => {}); } - // Stats are fire-and-forget — nice to have pre-cached but not worth blocking on. if (eventIdsToFetch.length > 0) { nostr.query( [ @@ -203,11 +199,6 @@ export function useFeed(tab: 'follows' | 'global') { .map((ev) => ({ event: ev, sortTimestamp: ev.created_at })); } - // Prefetch authors and stats in parallel before returning. - // This ensures cache is populated before NoteCards mount, so individual - // useAuthor/useEventStats calls hit cache and never open relay subscriptions. - await prefetchPageData(items, nostr, queryClient); - return items; }, getNextPageParam: (lastPage) => {