diff --git a/.changeset/atom-compare-undefined.md b/.changeset/atom-compare-undefined.md new file mode 100644 index 00000000..6f8969f5 --- /dev/null +++ b/.changeset/atom-compare-undefined.md @@ -0,0 +1,5 @@ +--- +'@tanstack/store': patch +--- + +Fix atoms skipping `compare` whenever their previous value was `undefined`. A computed atom that keeps returning `undefined` no longer notifies its subscribers on every recompute, and a custom `compare` now decides updates from an `undefined` value too. A computed atom's first computation still skips `compare`. diff --git a/packages/store/src/atom.ts b/packages/store/src/atom.ts index 1a29e607..c8198bd2 100644 --- a/packages/store/src/atom.ts +++ b/packages/store/src/atom.ts @@ -157,6 +157,7 @@ export function createAtom( ): Atom | ReadonlyAtom { const isComputed = typeof valueOrFn === 'function' const getter = valueOrFn as (prev?: T) => T + let isFirstComputation = isComputed // Create plain object atom const atom: InternalAtom = { @@ -219,7 +220,8 @@ export function createAtom( : getValue === undefined && isComputed ? getter(oldValue) : getValue! - if (oldValue === undefined || !compare(oldValue, newValue)) { + if (isFirstComputation || !compare(oldValue, newValue)) { + isFirstComputation = false atom._snapshot = newValue return true } diff --git a/packages/store/tests/undefined-snapshot.test.ts b/packages/store/tests/undefined-snapshot.test.ts new file mode 100644 index 00000000..7f2f0bb8 --- /dev/null +++ b/packages/store/tests/undefined-snapshot.test.ts @@ -0,0 +1,42 @@ +import { describe, expect, test, vi } from 'vitest' +import { createAtom } from '../src' + +describe('undefined snapshots', () => { + test('a computed atom that stays undefined does not notify', () => { + const source = createAtom(0) + const derived = createAtom(() => + source.get() > 10 ? source.get() : undefined, + ) + const fn = vi.fn() + derived.subscribe(fn) + + source.set(1) + source.set(2) + + expect(fn).not.toHaveBeenCalled() + }) + + test('compare still decides when the previous value is undefined', () => { + const atom = createAtom(undefined, { + compare: () => true, + }) + const fn = vi.fn() + atom.subscribe(fn) + + atom.set(1) + + expect(atom.get()).toBeUndefined() + expect(fn).not.toHaveBeenCalled() + }) + + test('compare is not called before a computed atom first computes', () => { + const source = createAtom(0) + const compare = vi.fn( + (prev: Array, next: Array) => prev.length === next.length, + ) + const derived = createAtom(() => [source.get()], { compare }) + + expect(derived.get()).toEqual([0]) + expect(compare).not.toHaveBeenCalled() + }) +})