From 55affec2594b9184040a3e168899bfd3ebb7cca2 Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Sun, 16 Aug 2026 11:50:14 -0700 Subject: [PATCH 1/2] tests: add suspense test --- packages/react-store/tests/index.test.tsx | 64 ++++++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/packages/react-store/tests/index.test.tsx b/packages/react-store/tests/index.test.tsx index e431e96d..942dc28e 100644 --- a/packages/react-store/tests/index.test.tsx +++ b/packages/react-store/tests/index.test.tsx @@ -1,4 +1,11 @@ -import { act, render, renderHook, waitFor } from '@testing-library/react' +import { Suspense, startTransition, useState, use } from 'react' +import { + act, + fireEvent, + render, + renderHook, + waitFor, +} from '@testing-library/react' import { userEvent } from '@testing-library/user-event' import { describe, expect, it, test, vi } from 'vitest' import { createAtom, createStore } from '@tanstack/store' @@ -141,6 +148,61 @@ describe('atom hooks', () => { expect(getByText('Renders: 2')).toBeInTheDocument() }) + it('useSelector keeps the committed selector active while a selector change suspends', async () => { + const atom = createAtom({ a: 0, b: 0 }) + const never = new Promise(() => {}) + + const selectA = (state: { a: number; b: number }) => state.a + const selectB = vi.fn((state: { a: number; b: number }) => state.b) + + function Value({ mode }: { mode: 'a' | 'b' }) { + const value = useSelector(atom, mode === 'a' ? selectA : selectB) + + // Suspend only after useSelector has rendered with the pending selector. + if (mode === 'b') { + use(never) + } + + return A:{value} + } + + function Comp() { + const [mode, setMode] = useState<'a' | 'b'>('a') + + return ( + <> + + Loading

}> + +
+ + ) + } + + const { getByRole, getByTestId } = render() + + expect(getByTestId('value')).toHaveTextContent('A:0') + + fireEvent.click(getByRole('button', { name: 'Switch' })) + + // The transition reached selectB but did not commit because it suspended. + await waitFor(() => expect(selectB).toHaveBeenCalled()) + expect(getByTestId('value')).toHaveTextContent('A:0') + + // Emulate a change to the atom while the transition is suspended. + act(() => { + atom.set({ a: 1, b: 0 }) + }) + + // The committed selectA subscription must observe the synchronous update. + expect(getByTestId('value')).toHaveTextContent('A:1') + }) + it('useAtom returns the current value and setter', () => { const atom = createAtom(0) const { result } = renderHook(() => useAtom(atom)) From 7aeba815123e0cc67563aba9ab78da1b01a87792 Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Sun, 16 Aug 2026 11:53:55 -0700 Subject: [PATCH 2/2] chore: fix eslint --- packages/react-store/tests/index.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-store/tests/index.test.tsx b/packages/react-store/tests/index.test.tsx index 942dc28e..5f36579b 100644 --- a/packages/react-store/tests/index.test.tsx +++ b/packages/react-store/tests/index.test.tsx @@ -1,4 +1,4 @@ -import { Suspense, startTransition, useState, use } from 'react' +import { Suspense, startTransition, use, useState } from 'react' import { act, fireEvent,