fix(timeline): keep prior batch profiles during users-batch key churn

As messages stream in, new author pubkeys join the users-batch query's
pubkey set and the queryKey ("users-batch", ...pubkeys) changes. React
Query treats the new key as a brand-new query and drops `data` to
undefined for a frame before the new batch resolves.

That single undefined frame is the root of both regressions tho saw on
first-load:

  (a) Content-bust: the consumer collapses the profile lookup to {} when
      data is undefined, so already-resolved rows revert to truncated
      pubkeys and drop their avatars mid-stream.
  (b) Gaping empty space: the near-empty rebuild hands MessageTimeline a
      new messages reference, churning the virtualizer's measurement
      cache and scrollMargin so older rows pop in one at a time above a
      hole.

Adding placeholderData: keepPreviousData (v5 function form) retains the
previously resolved batch through the key transition, so settled rows
stay settled while the new batch fetches in the background. Verified
empirically against the installed query-core v5.100.14: the immediate
frame after a new author arrives keeps the prior profiles instead of
going undefined.

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
This commit is contained in:
npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w
2026-06-16 00:19:54 -07:00
co-authored by Taylor Ho
parent 807c761bca
commit f1846e06cb
+15 -1
View File
@@ -1,5 +1,10 @@
import * as React from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import {
keepPreviousData,
useMutation,
useQuery,
useQueryClient,
} from "@tanstack/react-query";
import {
getProfile,
@@ -283,6 +288,15 @@ export function useUsersBatchQuery(
queryFn: () => getUsersBatch(normalizedPubkeys),
staleTime: 60_000,
gcTime: 5 * 60 * 1_000,
// As messages stream in, new author pubkeys join the set and the queryKey
// changes — React Query treats that as a brand-new query and would drop
// `data` to undefined for a frame. That collapses the consumer's profile
// lookup to {}, reverting already-resolved rows to truncated pubkeys with
// no avatar (content-bust) AND handing the timeline a near-empty messages
// array that churns the virtualizer's measurement cache (gaping space).
// keepPreviousData retains the prior resolved batch through the key
// transition so settled rows stay settled while the new batch fetches.
placeholderData: keepPreviousData,
});
// Seed individual "user-profile" cache entries so avatar clicks are instant