From 88d12a4adcaeb71a09688c8fb32726a9e2f7145b Mon Sep 17 00:00:00 2001 From: Sam Willis Date: Fri, 3 Oct 2025 17:00:25 +0100 Subject: [PATCH] fix query collection `.preload()` --- .changeset/light-moles-divide.md | 5 ++ packages/query-db-collection/src/query.ts | 7 +- .../query-db-collection/tests/query.test.ts | 70 +++++++++++++++++++ 3 files changed, 78 insertions(+), 4 deletions(-) create mode 100644 .changeset/light-moles-divide.md diff --git a/.changeset/light-moles-divide.md b/.changeset/light-moles-divide.md new file mode 100644 index 000000000..679cc72a2 --- /dev/null +++ b/.changeset/light-moles-divide.md @@ -0,0 +1,5 @@ +--- +"@tanstack/query-db-collection": patch +--- + +Fix collection.preload() hanging when called without startSync or subscribers. The QueryObserver now subscribes immediately when sync starts (from preload(), startSync, or first subscriber), while maintaining the staleTime behavior by dynamically unsubscribing when subscriber count drops to zero. diff --git a/packages/query-db-collection/src/query.ts b/packages/query-db-collection/src/query.ts index 832f6b755..2d53105cc 100644 --- a/packages/query-db-collection/src/query.ts +++ b/packages/query-db-collection/src/query.ts @@ -564,10 +564,9 @@ export function queryCollectionOptions( } } - // If startSync=true or there are subscribers to the collection, subscribe to the query straight away - if (config.startSync || collection.subscriberCount > 0) { - subscribeToQuery() - } + // Always subscribe when sync starts (this could be from preload(), startSync config, or first subscriber) + // We'll dynamically unsubscribe/resubscribe based on subscriber count to maintain staleTime behavior + subscribeToQuery() // Set up event listener for subscriber changes const unsubscribeFromCollectionEvents = collection.on( diff --git a/packages/query-db-collection/tests/query.test.ts b/packages/query-db-collection/tests/query.test.ts index 9a380547b..b87caf67c 100644 --- a/packages/query-db-collection/tests/query.test.ts +++ b/packages/query-db-collection/tests/query.test.ts @@ -2270,4 +2270,74 @@ describe(`QueryCollection`, () => { expect(collection.utils.isError()).toBe(true) }) }) + + describe(`preload()`, () => { + it(`should resolve preload() even without startSync or subscribers`, async () => { + const queryKey = [`preload-test`] + const items: Array = [ + { id: `1`, name: `Item 1` }, + { id: `2`, name: `Item 2` }, + ] + + const queryFn = vi.fn().mockResolvedValue(items) + + const config: QueryCollectionConfig = { + id: `preload-test`, + queryClient, + queryKey, + queryFn, + getKey, + // Note: NOT setting startSync: true + } + + const options = queryCollectionOptions(config) + const collection = createCollection(options) + + // Collection should be idle initially + expect(collection.status).toBe(`idle`) + expect(queryFn).not.toHaveBeenCalled() + + // Preload should resolve without any subscribers + await collection.preload() + + // After preload, collection should be ready and queryFn should have been called + expect(collection.status).toBe(`ready`) + expect(queryFn).toHaveBeenCalledTimes(1) + expect(collection.size).toBe(items.length) + expect(collection.get(`1`)).toEqual(items[0]) + expect(collection.get(`2`)).toEqual(items[1]) + }) + + it(`should not call queryFn multiple times if preload() is called concurrently`, async () => { + const queryKey = [`preload-concurrent-test`] + const items: Array = [{ id: `1`, name: `Item 1` }] + + const queryFn = vi.fn().mockResolvedValue(items) + + const config: QueryCollectionConfig = { + id: `preload-concurrent-test`, + queryClient, + queryKey, + queryFn, + getKey, + } + + const options = queryCollectionOptions(config) + const collection = createCollection(options) + + // Call preload() multiple times concurrently + const promises = [ + collection.preload(), + collection.preload(), + collection.preload(), + ] + + await Promise.all(promises) + + // queryFn should only be called once despite multiple preload() calls + expect(queryFn).toHaveBeenCalledTimes(1) + expect(collection.status).toBe(`ready`) + expect(collection.size).toBe(items.length) + }) + }) })