Skip to content

fix(core): stale computed values after a batch reverts a signal - #947

Merged
JoviDeCroock merged 1 commit into
mainfrom
fix/batch-revert-stale-computed
Jul 5, 2026
Merged

fix(core): stale computed values after a batch reverts a signal#947
JoviDeCroock merged 1 commit into
mainfrom
fix/batch-revert-stale-computed

Conversation

@JoviDeCroock

Copy link
Copy Markdown
Member

Root cause

reconcileBatchSnapshots rolls a signal's _version backwards when a batch ends with the value equal to its pre-batch state. Version numbers are the only invalidation mechanism, and they must be monotonic: a lazy (unsubscribed) computed that reads the signal during the batch records the intermediate version on its dependency node. After the rollback, the next distinct write re-mints that exact version number for a different value, so needsToRecompute sees matching versions and the computed serves its stale cache — permanently, on every read, even though the signal holds a new value:

const s = signal("A");
const c = computed(() => s.value + "!");
c.value;                                        // "A!"
batch(() => { s.value = "B"; c.value; s.value = "A"; });
s.value = "C";
c.value;                                        // "B!" — stale forever

Reading a modified signal inside a batch is explicitly supported per batch's documentation, and this shipped in @preact/signals-core@1.14.0.

Fix

Never roll versions back. Instead, when a batch reverts a value, fast-forward the subscriber nodes that last saw the pre-batch version to the current version. Subscribed effects/computeds still skip recomputing on no-op batches (the point of the original optimization), while any node that observed an intermediate version keeps its mismatch and recomputes correctly. Lazy computeds that saw only the pre-batch version now do one value-equal recompute instead of being skipped — trading a micro-optimization for correctness.

Note: this PR was authored by Claude and @JoviDeCroock.

Rolling the version back during batch snapshot reconciliation let a
later write re-mint a version number that a lazy computed had already
observed mid-batch, permanently serving a stale cached value. Fast-forward
subscriber nodes that saw the pre-batch version instead.
@netlify

netlify Bot commented Jul 5, 2026

Copy link
Copy Markdown

Deploy Preview for preact-signals-demo ready!

Name Link
🔨 Latest commit 5fab6c2
🔍 Latest deploy log https://app.netlify.com/projects/preact-signals-demo/deploys/6a49fe56c1c6ab00084ab7fd
😎 Deploy Preview https://deploy-preview-947--preact-signals-demo.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@changeset-bot

changeset-bot Bot commented Jul 5, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 5fab6c2

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
@preact/signals-core Patch
preact-signals-devtools Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

github-actions Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Size Change: +394 B (+0.22%)

Total Size: 180 kB

📦 View Changed
Filename Size Change
docs/dist/assets/bench-********.js 1.6 kB -1 B (-0.06%)
docs/dist/assets/devtools-********.js 911 B +3 B (+0.33%)
docs/dist/assets/EmbeddedDevtools-********.js 17.7 kB +23 B (+0.13%)
docs/dist/assets/index-********.js 1.09 kB +256 B (+30.59%) 🚨
docs/dist/assets/signals-core.module-********.js 1.92 kB +28 B (+1.48%)
docs/dist/assets/signals.module-********.js 2.65 kB +1 B (+0.04%)
docs/dist/assets/Unmount-********.js 654 B +4 B (+0.62%)
docs/dist/assets/utils.module-********.js 501 B +1 B (+0.2%)
docs/dist/basic-********.js 247 B +2 B (+0.82%)
docs/dist/nesting-********.js 1.15 kB +2 B (+0.17%)
docs/dist/react-********.js 242 B -1 B (-0.41%)
packages/core/dist/signals-core.js 1.94 kB +15 B (+0.78%)
packages/core/dist/signals-core.mjs 1.93 kB +20 B (+1.05%)
packages/devtools-ui/dist/devtools-ui.js 16.2 kB +20 B (+0.12%)
packages/devtools-ui/dist/devtools-ui.mjs 15.6 kB +21 B (+0.13%)
ℹ️ View Unchanged
Filename Size
docs/dist/assets/client-********.js 46.6 kB
docs/dist/assets/jsxRuntime.module-********.js 300 B
docs/dist/assets/preact.module-********.js 4.74 kB
docs/dist/assets/style-********.css 5.26 kB
packages/debug/dist/debug.js 4.64 kB
packages/debug/dist/debug.mjs 4.15 kB
packages/devtools-adapter/dist/devtools-adapter.js 2.36 kB
packages/devtools-adapter/dist/devtools-adapter.mjs 2.07 kB
packages/preact-transform/dist/signals-transform.js 1.3 kB
packages/preact-transform/dist/signals-transform.mjs 1.29 kB
packages/preact-transform/dist/signals-transform.umd.js 1.42 kB
packages/preact/dist/signals.js 1.81 kB
packages/preact/dist/signals.mjs 1.75 kB
packages/react-transform/dist/signals-transform.js 7.28 kB
packages/react-transform/dist/signals-transform.mjs 6.47 kB
packages/react-transform/dist/signals-transform.umd.js 7.39 kB
packages/react/dist/signals.js 214 B
packages/react/dist/signals.mjs 165 B
packages/vite-plugin/dist/vite-plugin.js 8.86 kB
packages/vite-plugin/dist/vite-plugin.mjs 7.86 kB

compressed-size-action

@JoviDeCroock
JoviDeCroock merged commit 2910fbf into main Jul 5, 2026
6 checks passed
@JoviDeCroock
JoviDeCroock deleted the fix/batch-revert-stale-computed branch July 5, 2026 16:35
@github-actions github-actions Bot mentioned this pull request Jul 5, 2026
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.

2 participants