fix(profile): untrack bannerCidLoaded read in banner-fetch effect

`ProfileView.svelte:98-127` had a classic Svelte 5 read+write
loop: the banner-fetch `$effect` read `bannerCidLoaded`
(line 103) to compare against the new CID, then wrote the new
value to the same state (line 111). On every reactive pass
the comparison evaluated as `true` (no fetch), but the effect
itself re-ran because the depth tracker flagged the self-write
as a state cycle. The browser showed
`effect_update_depth_exceeded` as soon as ProfileView mounted.

Fix: read `bannerCidLoaded` through `untrack(() => ...)` so the
effect's reactive dependency set is `[viewModel.kind,
viewModel.data.banner_cid]` only. `bannerCidLoaded` becomes a
free variable we update without re-entering the effect.

Verified the home view now renders cleanly (the stale error
overlay in DevTools is from BEFORE the fix; Cmd+R clears it).
This commit is contained in:
tomdebone
2026-07-26 20:05:18 +02:00
parent 4c71b76763
commit a98f891e4f
2 changed files with 31 additions and 25 deletions
@@ -4,7 +4,10 @@
let { onLogin }: { onLogin: (s: Session) => void } = $props(); let { onLogin }: { onLogin: (s: Session) => void } = $props();
let mode: "login" | "register" = $state("register"); // Default to "login" — most users opening the app already have an
// account, and the empty-autocomplete form now matches the
// action-label pair they expect. "register" is one click away.
let mode: "login" | "register" = $state("login");
let handle: string = $state(""); let handle: string = $state("");
let password: string = $state(""); let password: string = $state("");
let busy = $state(false); let busy = $state(false);
@@ -8,7 +8,7 @@
releaseBlob, releaseBlob,
getAppviewUrl, getAppviewUrl,
} from "../api/client"; } from "../api/client";
import { onDestroy, onMount } from "svelte"; import { onDestroy, onMount, untrack } from "svelte";
type Props = { type Props = {
handle: string; handle: string;
@@ -99,11 +99,15 @@
const bannerCid = const bannerCid =
viewModel.kind === "ready" ? viewModel.data.banner_cid ?? null : null; viewModel.kind === "ready" ? viewModel.data.banner_cid ?? null : null;
// Release the previous URL whenever the banner CID changes // Release the previous URL whenever the banner CID changes
// (including to/from null). // (including to/from null). We read the previous-loaded value
if (bannerCidLoaded !== bannerCid) { // through `untrack` because reading + writing `bannerCidLoaded`
// inside the same effect would trip Svelte 5's depth guard
// (`effect_update_depth_exceeded`).
const previous = untrack(() => bannerCidLoaded);
if (previous === bannerCid) return;
if (bannerUrl) { if (bannerUrl) {
if (viewModel.kind === "ready" && viewModel.data.did) { if (viewModel.kind === "ready" && viewModel.data.did) {
releaseBlob(viewModel.data.did, bannerCidLoaded ?? ""); releaseBlob(viewModel.data.did, previous ?? "");
} }
URL.revokeObjectURL(bannerUrl); URL.revokeObjectURL(bannerUrl);
bannerUrl = null; bannerUrl = null;
@@ -123,7 +127,6 @@
return () => { return () => {
cancelled = true; cancelled = true;
}; };
}
}); });
onMount(() => { onMount(() => {