Repository navigation
fix(hooks): a reader's own first build after mount doesn't run it again - #43
Merged
Merged
Conversation
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After mount the reader now drops only the announcement of a first build. A stale node its read rebuilds, as after a failed rebuild, carries a value its other reads haven't seen, so that announcement is delivered. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
jarrednorrisdev
added a commit
that referenced
this pull request
Oct 9, 2026
Its finalizer error escapes the provider's teardown and aborts the Svelte flush that unmounts it, leaving Svelte's scheduler broken for the rest of the page. Later tests in hooks.browser.test.ts then saw each $derived run again on every read, which failed #43's transform-once test when the branches were merged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…he page A node whose first build throws stays uninitialized, so the reader took its recovery for a first build and dropped it, though the page showed the failure. Only a node that hasn't failed counts as a first build now. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
jarrednorrisdev
added a commit
that referenced
this pull request
Oct 10, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
jarrednorrisdev
added a commit
that referenced
this pull request
Oct 10, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
jarrednorrisdev
added a commit
that referenced
this pull request
Oct 10, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
jarrednorrisdev
added a commit
that referenced
this pull request
Oct 10, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What broke
subscribedReaderdrops the announcement a reader's ownregistry.getmakes when it builds an atom for the first time, since that announcement carries the very value the read returns. It did this only before mount (if (mounted || reading !== current), from JND-95). After mount the announcement still reached the reader and re-ran it with an unchanged value. That contradicts theuseAtomValueJSDoc: "The transform runs again only when the atom, or state the transform reads, changes, so a transform that builds an object returns the same object until then."Scenarios:
useAtomValue(() => family(id), transform)withidswitching to an atom not built yet ran the transform twice for it (['a', 'b', 'b']) and returned a new object for an unchanged value.$effectthat reads an atom first built there ran twice ([1, 1]), with or without a transform.useAtomSuspenseof an atom that resolves synchronously, awaited in an$effect, ran the code after theawaittwice.The after-mount delivery was kept for JND-24's
afterMountpattern, which JND-98 later removed, so nothing relies on it now.Fix
if (reading !== current). After mount, the read setsreadingonly while it builds a node for the first time (see the follow-ups below). The comment there now explains why this is safe: only the build's own announcement is dropped. A value set during a registry batch is announced at commit, afterreadingis reset, and later changes arrive as usual.Step through one click: step 7 is where it goes wrong, and the same step with the fix.
Tests
Red without the fix in Chromium and WebKit, green with it:
$effectruns once while the atom is unchanged; an$effectthat first builds the atom it reads runs once.$effectruns the code after the await once.Guards, sync and async renders, passing before and after: a stream's sync emissions in the first build then an async one; a first build that sets another atom the component reads; a first build that sets an atom it read and so rebuilds; a first build that calls
setSelfat once and later; later changes of the switched-to atom still arriving after a switch.Library suites (Chromium, WebKit, server) 478/478, devtools 70/70, and format, lint and check are all clean.
Accepted edge cases
Merges
Brought up to date with
mainon 2026-10-10, after #39 and #42 changed the same file. Git merged it with no conflicts, and CI passed on the result.Follow-up: a stale node rebuilt by a later read
A review found that dropping every own-read announcement after mount lost one real update. If an atom's rebuild fails, the node stays stale. When a later imperative read (an event handler reading
.current) rebuilds it, that announcement carries a value the page hasn't shown. The reader dropped it, so the markup stayed on the old value. After mount, the reader now drops only the announcement of a first build, checked by whether the node is uninitialized before the read. Themountedflag is back for this. The SAFETY comment on the registry internals now namesfailedas private to Effect's node implementation;currentStateis on its publicNodeinterface. Test: "a failed rebuild that a later read recovers reaches the page".A node whose first build failed also counts: it stays uninitialized, but the page has shown the failure, so the reader only treats a node that hasn't failed as a first build. Test: "a failed first build that a later read recovers reaches the page".
🤖 Generated with Claude Code