From 7e3b7fe77f9ae07277400a91eb64524ede019873 Mon Sep 17 00:00:00 2001 From: Eli Mallon Date: Wed, 23 Sep 2026 13:20:10 -0700 Subject: [PATCH] app: stop refetching profiles Bluesky doesn't have useAvatars asked for every DID missing from the profile cache on every render, and callers pass a fresh array each render. A DID the Bluesky app view doesn't know (an account on another PDS with no Bluesky profile) never lands in the cache, while getProfiles replaced the cache object even when it got nothing back. So a stream page for such an account refetched and re-rendered in a loop until React gave up with "Maximum update depth exceeded" and the screen went white. The Android e2e suite hit it intermittently: its accounts live on a local PDS. Ask for each DID once, keyed on the DIDs rather than the array, and leave the cache alone when a lookup returns nothing. Co-Authored-By: Claude Opus 5.5 (1M context) --- js/app/hooks/useAvatars.tsx | 24 ++++++++++++++---------- js/app/store/slices/blueskySlice.ts | 6 +++++- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/js/app/hooks/useAvatars.tsx b/js/app/hooks/useAvatars.tsx index c11015e37..4a0f8b487 100644 --- a/js/app/hooks/useAvatars.tsx +++ b/js/app/hooks/useAvatars.tsx @@ -1,5 +1,5 @@ import { ProfileViewDetailed } from "@atproto/api/dist/client/types/app/bsky/actor/defs"; -import { useEffect, useMemo } from "react"; +import { useEffect, useRef } from "react"; import { useStore } from "store"; import { useCachedProfiles } from "store/hooks"; @@ -8,17 +8,21 @@ export default function useAvatars(dids: string[]) { const getProfiles = useStore((state) => state.getProfiles); const profiles: Record = useCachedProfiles(); - const missingDids = useMemo( - () => dids.filter((did) => !(did in profiles)), - [dids, profiles], - ); + // Ask for each DID once. Callers pass a fresh array every render, and a DID + // the Bluesky app view doesn't know (an account on another PDS with no + // Bluesky profile) never lands in the cache, so keying on the array, or + // re-asking for whatever is missing, refetches in a loop. + const requested = useRef(new Set()); + const missing = dids + .filter((did) => !(did in profiles) && !requested.current.has(did)) + .join(","); useEffect(() => { - if (missingDids.length > 0) { - console.log("Fetching profiles for DIDs:", missingDids); - getProfiles(missingDids).then((e) => console.log("ok", e)); - } - }, [missingDids]); + if (!missing) return; + const toFetch = missing.split(","); + toFetch.forEach((did) => requested.current.add(did)); + getProfiles(toFetch); + }, [missing]); return profiles; } diff --git a/js/app/store/slices/blueskySlice.ts b/js/app/store/slices/blueskySlice.ts index 40ee76a7e..d87d9be3d 100644 --- a/js/app/store/slices/blueskySlice.ts +++ b/js/app/store/slices/blueskySlice.ts @@ -450,10 +450,14 @@ export const createBlueskySlice: StateCreator< const bskyAgent = new Agent("https://public.api.bsky.app"); const payload = await bskyAgent.getProfiles({ actors }); let parsedProfiles = {}; - console.log(payload); payload.data.profiles.forEach((p) => { parsedProfiles[p.did] = p; }); + // an unchanged cache must stay the same object, or every subscriber + // re-renders for nothing + if (Object.keys(parsedProfiles).length === 0) { + return; + } set((s) => ({ profileCache: { ...(s as BlueskySlice).profileCache, -- 2.51.2