fix(postcard): remove sync effects that looped on like/repost click
`PostCard.svelte` had two `$effect`s (line 140 + 152) that
persisted `liked` / `reposted` state into a `useLocalStorage`
box via `box.set(...)`. The pre effect (line 126) created a
fresh box on every post-prop change and read its stored value
into the local `liked` / `likedUri` $states. The sync effects
then noticed the mismatch between the in-memory state and the
box's internal `current` and called `set` to reconcile.
When the user clicked the heart, `liked` flipped and `likeDelta`
incremented. The sync effect re-ran, called `box.set({liked,
uri: likedUri})`, which mutated the box's closure `current`.
In Svelte 5 the depth tracker flagged the re-entry as
`effect_update_depth_exceeded` once the user clicked enough
times to exceed the per-tick limit. The error was caught by the
Svelte error boundary and shown as a red overlay; the page kept
rendering but the like-state path was broken.
Fix:
* Wrap the pre effect's writes in `untrack(() => ...)` so its
reactive dep set is just `[post.did, post.rkey]` — without
untrack, every `liked = ...` would re-enter the effect.
* Drop both sync effects entirely. localStorage writes now
happen directly in the click handler (`likedBox?.set(...)`)
and on rollback — no Svelte state is touched by the box's
internal updates.
Also:
* Settings view reworked to X-style: sectioned cards with
label-left / value-right rows, clickable action rows with
right-side hints ("atproto", "↗ bsky.app"), and a separate
red danger zone for sign out. Stays monospace + orange
accent + `//` terminal comments.
`npm run check` 0 errors. `npm run test` 20/20 passing. The
error no longer fires when clicking the heart.
This commit is contained in:
@@ -124,36 +124,27 @@
|
||||
> | null = $state(null);
|
||||
|
||||
$effect.pre(() => {
|
||||
// Restore like/repost state from localStorage on mount and on
|
||||
// every post-prop change (so navigating between posts restores
|
||||
// the right per-post state). The writes are inside `untrack` so
|
||||
// the effect's reactive dep set is just `[post.did, post.rkey]`
|
||||
// — without untrack, each write to `liked` / `reposted` / etc.
|
||||
// would re-enter the effect and the depth tracker would abort
|
||||
// with `effect_update_depth_exceeded` as soon as the user
|
||||
// clicks the heart or repost button.
|
||||
const likeKey = localStorageKey(`liked:${post.did}:${post.rkey}`);
|
||||
const repostKey = localStorageKey(`reposted:${post.did}:${post.rkey}`);
|
||||
likedBox = useLocalStorage(likeKey, { liked: false, uri: null });
|
||||
const storedLike = likedBox.get();
|
||||
liked = storedLike.liked;
|
||||
likedUri = storedLike.uri;
|
||||
untrack(() => {
|
||||
likedBox = useLocalStorage(likeKey, { liked: false, uri: null });
|
||||
const storedLike = likedBox.get();
|
||||
liked = storedLike.liked;
|
||||
likedUri = storedLike.uri;
|
||||
|
||||
repostedBox = useLocalStorage(repostKey, { reposted: false, uri: null });
|
||||
const storedRepost = repostedBox.get();
|
||||
reposted = storedRepost.reposted;
|
||||
repostUri = storedRepost.uri;
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
// Skip the write when the box's stored value already matches the
|
||||
// current state — on first mount, `$effect.pre` reads the stored
|
||||
// value into `liked` / `likedUri`, and without this guard the very
|
||||
// first reactive pass would re-write the same bytes and trigger
|
||||
// an empty `notify()` to subscribers.
|
||||
if (!likedBox) return;
|
||||
const current = likedBox.get();
|
||||
if (current.liked === liked && current.uri === likedUri) return;
|
||||
likedBox.set({ liked, uri: likedUri });
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (!repostedBox) return;
|
||||
const current = repostedBox.get();
|
||||
if (current.reposted === reposted && current.uri === repostUri) return;
|
||||
repostedBox.set({ reposted, uri: repostUri });
|
||||
repostedBox = useLocalStorage(repostKey, { reposted: false, uri: null });
|
||||
const storedRepost = repostedBox.get();
|
||||
reposted = storedRepost.reposted;
|
||||
repostUri = storedRepost.uri;
|
||||
});
|
||||
});
|
||||
|
||||
async function onLikeClick(event: MouseEvent) {
|
||||
@@ -168,11 +159,16 @@
|
||||
const previousDelta = likeDelta;
|
||||
liked = !wasLiked;
|
||||
likeDelta += wasLiked ? -1 : 1;
|
||||
// Persist the optimistic state — the localStorage write is
|
||||
// synchronous so a page reload reflects the in-flight like even
|
||||
// if the server roundtrip is still pending.
|
||||
likedBox?.set({ liked, uri: likedUri });
|
||||
try {
|
||||
if (wasLiked) {
|
||||
if (!likedUri) {
|
||||
liked = wasLiked;
|
||||
likeDelta = previousDelta;
|
||||
likedBox?.set({ liked, uri: likedUri });
|
||||
showError("can't unlike: missing like URI");
|
||||
return;
|
||||
}
|
||||
@@ -182,9 +178,11 @@
|
||||
const response = await likePost(post.uri, post.cid);
|
||||
likedUri = response.uri;
|
||||
}
|
||||
likedBox?.set({ liked, uri: likedUri });
|
||||
} catch (error) {
|
||||
liked = wasLiked;
|
||||
likeDelta = previousDelta;
|
||||
likedBox?.set({ liked, uri: likedUri });
|
||||
showError(`like failed: ${error}`);
|
||||
} finally {
|
||||
likeBusy = false;
|
||||
@@ -203,11 +201,13 @@
|
||||
const previousDelta = repostDelta;
|
||||
reposted = !wasReposted;
|
||||
repostDelta += wasReposted ? -1 : 1;
|
||||
repostedBox?.set({ reposted, uri: repostUri });
|
||||
try {
|
||||
if (wasReposted) {
|
||||
if (!repostUri) {
|
||||
reposted = wasReposted;
|
||||
repostDelta = previousDelta;
|
||||
repostedBox?.set({ reposted, uri: repostUri });
|
||||
showError("can't unrepost: missing repost URI");
|
||||
return;
|
||||
}
|
||||
@@ -217,9 +217,11 @@
|
||||
const response = await repostPost(post.uri, post.cid);
|
||||
repostUri = response.uri;
|
||||
}
|
||||
repostedBox?.set({ reposted, uri: repostUri });
|
||||
} catch (error) {
|
||||
reposted = wasReposted;
|
||||
repostDelta = previousDelta;
|
||||
repostedBox?.set({ reposted, uri: repostUri });
|
||||
showError(`repost failed: ${error}`);
|
||||
} finally {
|
||||
repostBusy = false;
|
||||
|
||||
Reference in New Issue
Block a user