Skip to content

fix(demo): keep CauseView's rows stable when a batch commits - #44

Merged
jarrednorrisdev merged 1 commit into
mainfrom
fix/demo-cause-view-keys
Oct 10, 2026
Merged

jarrednorrisdev merged 1 commit into
mainfrom
fix/demo-cause-view-keys

Conversation

@jarrednorrisdev

@jarrednorrisdev jarrednorrisdev commented Oct 9, 2026 •

Copy link
Copy Markdown
Owner

Fixes an uncaught error on the docs site's RPC page. Only the demo changes; no package is released.

CauseView is the docs site's component that shows an Effect Cause (the record of why an effect failed) as one row per reason.

Clicking "Todo 99" throws in the console

<!-- apps/demo/src/routes/rpc/lookup.svelte: the query-family example -->
<CauseView cause={result.cause} code label={call} />

<!-- inside CauseView, before this PR -->
<script>
  let generation = 0;
  const keyed = $derived.by(() => {
    generation += 1; // a side effect: every evaluation makes new keys
    return rows.map((row, index) => ({ ...row, key: `${generation}-${index}` }));
  });
</script>

{#each keyed as row (row.key)} … {/each}

$derived (Svelte 5) is a value Svelte recalculates when what it reads changes, and Svelte expects it to have no side effects. {#each list as row (row.key)} is a keyed list: Svelte matches each rendered row to an item by its key.

Bug: clicking Todo 99 logs Cannot read properties of undefined (reading 'e'). It happens on every click in the production build, and in vite dev when requests overlap (Todo 2, then Todo 99 while the first is still loading). The page still shows the right result. The error comes from async Svelte (experimental.async, which this site turns on): when it applies a batch of updates, the keyed list reads keyed again, keyed evaluates again and the counter goes up. So the keys it gets back match none of the rows already on screen, and Svelte's row matching (reconcile in each.js) looks up a row that isn't there.

Fix: the list sits in a {#key cause} block and its rows need no keys. {#key value} destroys and rebuilds what it wraps whenever value changes. So a new cause still rebuilds the list and its rows still slide in, while reading the same cause again changes nothing.

- let generation = 0;
- const keyed = $derived.by(() => {
-   generation += 1;
-   return rows.map((row, index) => ({ ...row, key: `${generation}-${index}` }));
- });
…
- {#each keyed as row (row.key)}
+ {#key cause}
+   {#each rows as row}
      …
+   {/each}
+ {/key}

Tests

The e2e test "RPC page › add, typed error, toggle and the query family" now records the page's errors and ends with expect(await errors()).toEqual([]). Without the fix it fails, logging exactly that error (chromium, production build). svelte-check on the demo: 0 errors, 0 warnings. The full e2e suite passes in CI in chromium, firefox and webkit with this change.

Decisions

  • {#key cause} instead of keying rows by the cause object. The first version of this PR kept the keyed list and gave each cause its own number through a WeakMap<Cause, number>. That worked, but it needed a counter, a map and a helper to get the same effect a {#key} block gets with no script at all.
  • Left alone: a dev-only await_reactivity_loss warning on /suspense. It comes from Svelte's own $effect.pending() runtime, not from this repo.
Where to look in the diff
  • apps/demo/src/lib/docs/kit/cause-view.svelte: removes keyed and generation; the list is wrapped in {#key cause}.
  • apps/demo/e2e/demo.test.ts: "RPC page › add, typed error, toggle and the query family" uses watch(page) and checks errors().
  • .changeset/quick-mammals-smoke.md: an empty changeset, because only the demo changes.

🤖 Generated with Claude Code

CauseView keyed its rows with a counter bumped on every evaluation of a
$derived. With async Svelte, a keyed each block re-reads its list when a batch
commits, got keys none of its rows had, and threw "Cannot read properties of
undefined (reading 'e')". It happened on the RPC page's "Todo 99" button, every
time in production, and in dev with overlapping requests. The list now sits in
a {#key cause} block, so a new cause remounts it (its rows still slide in) and
the rows need no keys. The e2e test checks the page logs no errors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jarrednorrisdev
jarrednorrisdev force-pushed the fix/demo-cause-view-keys branch from 2823609 to b7d3ce1 Compare October 10, 2026 00:57
@jarrednorrisdev jarrednorrisdev changed the title fix(demo): keep CauseView's row keys stable when a batch commits fix(demo): keep CauseView's rows stable when a batch commits Oct 10, 2026
@jarrednorrisdev
jarrednorrisdev marked this pull request as ready for review October 10, 2026 01:02
@jarrednorrisdev
jarrednorrisdev merged commit 660d92f into main Oct 10, 2026
16 checks passed
@jarrednorrisdev
jarrednorrisdev deleted the fix/demo-cause-view-keys branch October 10, 2026 01:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant