diff --git a/.changeset/bright-beds-reflect.md b/.changeset/bright-beds-reflect.md new file mode 100644 index 0000000000..f4da834616 --- /dev/null +++ b/.changeset/bright-beds-reflect.md @@ -0,0 +1,5 @@ +--- +'@tanstack/react-db': patch +--- + +Refresh live query snapshots immediately after subscribing, even when the collection is still loading. diff --git a/packages/react-db/src/useLiveQuery.ts b/packages/react-db/src/useLiveQuery.ts index 331ff3a279..663a13caec 100644 --- a/packages/react-db/src/useLiveQuery.ts +++ b/packages/react-db/src/useLiveQuery.ts @@ -439,11 +439,13 @@ export function useLiveQuery( versionRef.current += 1 onStoreChange() }) - // Collection may be ready and will not receive initial `subscribeChanges()` - if (collectionRef.current.status === `ready`) { - versionRef.current += 1 - onStoreChange() - } + // The collection may have changed between render-time getSnapshot() and + // this subscription being attached. This is common when an on-demand query + // hydrates local rows immediately but keeps the collection loading while + // remote sync finishes. Force one post-subscribe snapshot refresh + // regardless of status so React sees those rows. + versionRef.current += 1 + onStoreChange() return () => { subscription.unsubscribe() } diff --git a/packages/react-db/tests/useLiveQuery.test.tsx b/packages/react-db/tests/useLiveQuery.test.tsx index fbb48d882c..4ad2c25093 100644 --- a/packages/react-db/tests/useLiveQuery.test.tsx +++ b/packages/react-db/tests/useLiveQuery.test.tsx @@ -18,6 +18,7 @@ import { mockSyncCollectionOptions, stripVirtualProps, } from '../../db/tests/utils' +import type { SyncConfig } from '@tanstack/db' type Person = { id: string @@ -1559,6 +1560,56 @@ describe(`Query Collections`, () => { }) describe(`eager execution during sync`, () => { + it(`refreshes the snapshot after subscribing while the collection is still loading`, async () => { + type TestRow = { id: string; value: number } + + let syncBegin: (() => void) | undefined + let syncWrite: Parameters[`sync`]>[0][`write`] + let syncCommit: (() => void) | undefined + let didWriteAfterSnapshot = false + + const collection = createCollection({ + id: `loading-subscribe-snapshot-refresh`, + getKey: (row) => row.id, + startSync: false, + sync: { + sync: ({ begin, write, commit }) => { + syncBegin = begin + syncWrite = write + syncCommit = commit + }, + }, + }) + + const { result } = renderHook(() => { + const queryResult = useLiveQuery(collection) + + if (!didWriteAfterSnapshot) { + didWriteAfterSnapshot = true + // Simulate a collection receiving rows after useLiveQuery read its + // render-time snapshot, but before React attached the external-store + // subscription. The collection intentionally stays loading because + // the missed update should still be visible before remote sync + // finishes. + syncBegin!() + syncWrite({ + type: `insert`, + value: { id: `1`, value: 1 }, + }) + syncCommit!() + } + + return queryResult + }) + + await waitFor(() => { + expect( + result.current.data.map((row) => stripVirtualProps(row)), + ).toEqual([{ id: `1`, value: 1 }]) + }) + expect(result.current.isLoading).toBe(true) + }) + it(`should show state while isLoading is true during sync`, async () => { let syncBegin: (() => void) | undefined let syncWrite: ((op: any) => void) | undefined