From de74030d53412ac0e3c417f1e2c26e062c875c89 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 27 Oct 2026 10:41:01 +0000 Subject: [PATCH 1/2] fix(store): apply atom.set(undefined) to mutable atoms #289 made mutable atoms skip any `_update` call whose argument is `undefined`, so the reactive graph's no-argument recompute calls could not clear them. `set(undefined)` passes that same `undefined` as the new value, so it was silently dropped and the atom kept its previous value. Check the argument count instead. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01BvSDyHMgrdgjFz8EAk5VKo --- .changeset/atom-set-undefined.md | 5 +++++ packages/store/src/atom.ts | 5 +++-- packages/store/tests/store.test.ts | 14 +++++++++++++- 3 files changed, 21 insertions(+), 3 deletions(-) create mode 100644 .changeset/atom-set-undefined.md diff --git a/.changeset/atom-set-undefined.md b/.changeset/atom-set-undefined.md new file mode 100644 index 00000000..475ec8ef --- /dev/null +++ b/.changeset/atom-set-undefined.md @@ -0,0 +1,5 @@ +--- +'@tanstack/store': patch +--- + +Fix `atom.set(undefined)` being ignored. A mutable atom now takes `undefined` as its new value, and only an internal `_update()` call made with no argument is still skipped. diff --git a/packages/store/src/atom.ts b/packages/store/src/atom.ts index 1a29e607..56c39b28 100644 --- a/packages/store/src/atom.ts +++ b/packages/store/src/atom.ts @@ -203,9 +203,10 @@ export function createAtom( activeSub = atom ++cycle atom.depsTail = undefined - } else if (getValue === undefined) { + } else if (arguments.length === 0) { // Mutable atoms can be marked dirty by the reactive graph, but they should - // never be recomputed without an explicit value/updater. + // never be recomputed without an explicit value/updater. Check the argument + // count rather than `undefined`, which `set(undefined)` passes as a value. return false } if (isComputed) { diff --git a/packages/store/tests/store.test.ts b/packages/store/tests/store.test.ts index 7c088144..142d1ffc 100644 --- a/packages/store/tests/store.test.ts +++ b/packages/store/tests/store.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test, vi } from 'vitest' // import { Store } from '../src/index' -import { createStore } from '../src' +import { createAtom, createStore } from '../src' describe('store', () => { test(`should set the initial value`, () => { @@ -116,4 +116,16 @@ describe('store', () => { expect(atom._update()).toBe(false) expect(store.state).toEqual({ pageIndex: 1 }) }) + + test('mutable atom can be set to undefined', () => { + const atom = createAtom(1) + const fn = vi.fn() + atom.subscribe(fn) + + atom.set(undefined) + + expect(atom.get()).toBeUndefined() + expect(fn).toHaveBeenCalledTimes(1) + expect(fn).toHaveBeenCalledWith(undefined) + }) }) From 2bc871d7e860af782ed4891f91584d3f6f052ef6 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 27 Oct 2026 12:57:24 +0000 Subject: [PATCH 2/2] fix(store): keep repeated set(undefined) from notifying Admitting an explicit `undefined` exposed the `oldValue === undefined` shortcut in `_update`, which counts any write over an `undefined` snapshot as a change and skips `compare`. Setting `undefined` on an atom that already held it notified subscribers again. Limit the shortcut to computed atoms, whose `undefined` snapshot means it has not computed yet. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01BvSDyHMgrdgjFz8EAk5VKo --- .changeset/atom-set-undefined.md | 2 +- packages/store/src/atom.ts | 7 ++++++- packages/store/tests/store.test.ts | 1 + 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.changeset/atom-set-undefined.md b/.changeset/atom-set-undefined.md index 475ec8ef..cdaab220 100644 --- a/.changeset/atom-set-undefined.md +++ b/.changeset/atom-set-undefined.md @@ -2,4 +2,4 @@ '@tanstack/store': patch --- -Fix `atom.set(undefined)` being ignored. A mutable atom now takes `undefined` as its new value, and only an internal `_update()` call made with no argument is still skipped. +Fix `atom.set(undefined)` being ignored. A mutable atom now takes `undefined` as its new value and compares it like any other, so setting `undefined` again does not notify subscribers. Only an internal `_update()` call made with no argument is still skipped. diff --git a/packages/store/src/atom.ts b/packages/store/src/atom.ts index 56c39b28..be69b62e 100644 --- a/packages/store/src/atom.ts +++ b/packages/store/src/atom.ts @@ -220,7 +220,12 @@ export function createAtom( : getValue === undefined && isComputed ? getter(oldValue) : getValue! - if (oldValue === undefined || !compare(oldValue, newValue)) { + // A computed atom treats an `undefined` snapshot as not computed yet. A + // mutable atom's `undefined` is a value like any other. + if ( + (isComputed && oldValue === undefined) || + !compare(oldValue, newValue) + ) { atom._snapshot = newValue return true } diff --git a/packages/store/tests/store.test.ts b/packages/store/tests/store.test.ts index 142d1ffc..f1b17b1b 100644 --- a/packages/store/tests/store.test.ts +++ b/packages/store/tests/store.test.ts @@ -122,6 +122,7 @@ describe('store', () => { const fn = vi.fn() atom.subscribe(fn) + atom.set(undefined) atom.set(undefined) expect(atom.get()).toBeUndefined()