Skip to content

Commit 23451a3

Browse files
jaysin586claude
andcommitted
fix(toc): break the activeHeading self-cycle in TableOfContentsV2
The effect read `activeHeading` inside the `if (next !== activeHeading)` guard *and* wrote it unconditionally to `''` at the top of every run. Reading + writing the same state inside a single effect makes Svelte 5 schedule a re-run that hits the same path — i.e. infinite recursion flagged as `effect_update_depth_exceeded`. Consumer pages crashed on hydration once headings populated. Wrap both the unconditional reset and the guarded write in `untrack` so the effect doesn't subscribe to its own writes. Template consumers of `activeHeading` still update because `untrack` only suppresses the reactive dependency tracking — the assignment itself still notifies downstream readers. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 70ad2be commit 23451a3

1 file changed

Lines changed: 17 additions & 3 deletions

File tree

‎src/lib/components/TableOfContentsV2.svelte‎

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
-->
1212
<script lang="ts">
1313
import { afterNavigate } from '$app/navigation'
14+
import { untrack } from 'svelte'
1415
import type { TocHeading } from '../utils/headings.js'
1516
1617
const { headings = [] } = $props<{ headings: TocHeading[] }>()
@@ -36,10 +37,18 @@
3637
return headings[0]?.id ?? ''
3738
}
3839
39-
/** Apply the new active id, skipping the rune write when nothing changed. */
40+
/** Apply the new active id, skipping the rune write when nothing changed.
41+
* The `activeHeading` read inside the guard is wrapped in `untrack` so
42+
* callers from inside a `$effect` don't accumulate a self-cycle:
43+
* reading `activeHeading` would otherwise subscribe the calling effect
44+
* to its own write, and the unconditional reset on effect start would
45+
* re-trigger every run. The on-screen TOC state still updates because
46+
* the assignment notifies *other* consumers (the template). */
4047
function updateActiveHeading() {
4148
const next = computeActive()
42-
if (next !== activeHeading) activeHeading = next
49+
untrack(() => {
50+
if (next !== activeHeading) activeHeading = next
51+
})
4352
}
4453
4554
function getScrollContainer(start: HTMLElement | undefined): HTMLElement | Window {
@@ -59,7 +68,12 @@
5968
}
6069
6170
$effect(() => {
62-
activeHeading = ''
71+
// Reset on route change so the previous page's active id doesn't
72+
// carry over. Wrapped in `untrack` so this write doesn't subscribe
73+
// the effect to its own future writes (which would cycle).
74+
untrack(() => {
75+
activeHeading = ''
76+
})
6377
updateActiveHeading()
6478
6579
// rAF-throttle scroll/resize: at most one layout read per frame.

0 commit comments

Comments
 (0)