diff --git a/.changeset/wet-hairs-lick.md b/.changeset/wet-hairs-lick.md new file mode 100644 index 0000000..eff4301 --- /dev/null +++ b/.changeset/wet-hairs-lick.md @@ -0,0 +1,7 @@ +--- +'@tanstack/store': patch +--- + +Fix a regression where mutable atoms could be updated internally with no updater and have their snapshot replaced with `undefined`. + +Mutable atoms now ignore internal no-argument `_update()` calls, while computed atoms keep existing recomputation behavior. This prevents external-store state from disappearing during reactive graph cleanup. diff --git a/packages/store/src/atom.ts b/packages/store/src/atom.ts index 1aadc08..624c35b 100644 --- a/packages/store/src/atom.ts +++ b/packages/store/src/atom.ts @@ -176,9 +176,15 @@ export function createAtom( _update(getValue?: T | ((snapshot: T) => T)): boolean { const prevSub = activeSub const compare = options?.compare ?? Object.is - activeSub = atom - ++cycle - atom.depsTail = undefined + if (isComputed) { + activeSub = atom + ++cycle + atom.depsTail = undefined + } else if (getValue === undefined) { + // Mutable atoms can be marked dirty by the reactive graph, but they should + // never be recomputed without an explicit value/updater. + return false + } if (isComputed) { atom.flags = ReactiveFlags.Mutable | ReactiveFlags.RecursedCheck } diff --git a/packages/store/tests/store.test.ts b/packages/store/tests/store.test.ts index 0c2903f..ab8d47b 100644 --- a/packages/store/tests/store.test.ts +++ b/packages/store/tests/store.test.ts @@ -67,4 +67,12 @@ describe('store', () => { store.setState(() => 24) expect(fn).toBeCalledWith({ prevVal: 12, currentVal: 24 }) }) + + test('mutable store should ignore internal no-arg updates', () => { + const store = createStore({ pageIndex: 1 }) + const atom = (store as any).atom + + expect(atom._update()).toBe(false) + expect(store.state).toEqual({ pageIndex: 1 }) + }) })