You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
fix(signals): guard node-less store staging reads - #4681
After #4625, reading latest(data) beside untrack(() => state.count) can throw TypeError: Cannot read properties of undefined (reading '_x') and halt reactivity when the store has no reactive node. The same program passes on #4625's direct parent.
stagedScreen accepts an optional signal, and the node-less store path deliberately omits it. Guard the new signal-specific holdStagingHook call when that argument is absent. This restores the previous node-less path while retaining #4625's hook for real signals, with no added state or allocation.
Single-button JSX repro, pinned to 2.0.0-rc.21. Click Run once: expected eventual 1 / 1; actual 0 / 0, the TypeError and REACTIVITY_HALTED. Removing only untrack is a passing control.
Adds render-effect and memo regressions, paired tracked-store controls, and a patch changeset. Each test checks the settled output and a second update.
How did you test this change?
On unchanged next@2105ffb8, both untracked regressions fail with the reported TypeError and both tracked controls pass. All four pass with the guard.
Signals builds for all three tiers and source declarations pass: npm run build:js and npm run types in packages/signals.
Full signals suite after the build: 479 files / 6,506 tests pass, with 14 existing expected failures and two skips (vitest run --maxWorkers=2).
The actual JSX repro and tracked control pass in Chromium across production, development and observe builds: six browser cases.
Prettier, git diff --check, and node scripts/check-release-invariants.mjs pass. A separate test-tree TypeScript check reports the same 43 existing diagnostics on base and candidate, with no new diagnostics.
Rolldown 1.2.11: the isPending/latest scenario adds 3 minified bytes and is 1 Brotli byte smaller. The other three standard signals scenarios are unchanged. All four comparison gates pass; no caps changed.
Local Rolldown-bundled production timings on Node 24.19.0 covered chains, verdict reads and tracked store updates, with five warm-up batches and 11 alternating measured rounds. Results were noisy: the store median ranged from -0.8% to +9.0% across the initial runs. A fresh store-only process with 19 alternating rounds of 400,000 updates, and GC outside timed batches, measured +1.2%. The 9% result did not reproduce consistently; these timings are inconclusive for small performance effects.
System calls cannot be simulated consistently and are left out of the measure. The reported value understates the real cost. Switch to the Walltime instrument to measure them.
Thanks @GabbeV — great find and repro. Your fix landed as part of #4682, with your commit (68be911d) kept as-is. On top of it, we extended the guard so node-less store keys also get the #4544 rule: an inherited-posture pass is now served the hold's own write, the same as it would be from a signal or a key with a node. The guard alone served the committed value there, which tore the landing frame (posture-store-parity S9 pins it). Closing in favor of that PR.
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
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.
Summary
After #4625, reading
latest(data)besideuntrack(() => state.count)can throwTypeError: Cannot read properties of undefined (reading '_x')and halt reactivity when the store has no reactive node. The same program passes on #4625's direct parent.stagedScreenaccepts an optional signal, and the node-less store path deliberately omits it. Guard the new signal-specificholdStagingHookcall when that argument is absent. This restores the previous node-less path while retaining #4625's hook for real signals, with no added state or allocation.Single-button JSX repro, pinned to 2.0.0-rc.21. Click Run once: expected eventual
1 / 1; actual0 / 0, the TypeError andREACTIVITY_HALTED. Removing onlyuntrackis a passing control.Adds render-effect and memo regressions, paired tracked-store controls, and a patch changeset. Each test checks the settled output and a second update.
How did you test this change?
next@2105ffb8, both untracked regressions fail with the reported TypeError and both tracked controls pass. All four pass with the guard.npm run build:jsandnpm run typesinpackages/signals.vitest run --maxWorkers=2).git diff --check, andnode scripts/check-release-invariants.mjspass. A separate test-tree TypeScript check reports the same 43 existing diagnostics on base and candidate, with no new diagnostics.