From 82dfb400212ea3bf59a239f8398c54b2b17fd7a1 Mon Sep 17 00:00:00 2001 From: Kyle Mathews Date: Fri, 10 Jul 2026 08:55:07 -0600 Subject: [PATCH 1/4] Apply query options review fixes --- .changeset/query-options-pass-through.md | 5 + docs/collections/query-collection.md | 40 +++++-- packages/query-db-collection/src/query.ts | 58 +++++++++- .../query-db-collection/tests/query.test-d.ts | 40 +++++++ .../query-db-collection/tests/query.test.ts | 108 ++++++++++++++++++ 5 files changed, 238 insertions(+), 13 deletions(-) create mode 100644 .changeset/query-options-pass-through.md diff --git a/.changeset/query-options-pass-through.md b/.changeset/query-options-pass-through.md new file mode 100644 index 0000000000..800936a9cd --- /dev/null +++ b/.changeset/query-options-pass-through.md @@ -0,0 +1,5 @@ +--- +"@tanstack/query-db-collection": minor +--- + +Add `queryOptions` pass-through support for Query Collection observer options while preserving existing top-level option precedence and QueryClient defaultOptions behavior. diff --git a/docs/collections/query-collection.md b/docs/collections/query-collection.md index 6c90f53dfb..92c509ecb7 100644 --- a/docs/collections/query-collection.md +++ b/docs/collections/query-collection.md @@ -56,9 +56,9 @@ The `queryCollectionOptions` function accepts the following options: ### Query Options -Query Collections use TanStack Query internally, but `queryCollectionOptions` is not a full `QueryObserverOptions` pass-through. It exposes only the Query options supported by the collection adapter. Fields that affect row materialization, collection identity, and synchronization are handled by the adapter itself. +Query Collections use TanStack Query internally. Common Query options can be provided either as existing top-level Query Collection options or through the additive `queryOptions` pass-through object. -The following Query options are forwarded to the underlying Query observer: +The following top-level Query Collection options are forwarded to the underlying Query observer: - `select`: Function that extracts the row array TanStack DB materializes from a wrapped Query response - `enabled`: Whether the query should automatically run (default: `true`) @@ -69,7 +69,26 @@ The following Query options are forwarded to the underlying Query observer: - `gcTime`: How long unused query data stays in the Query cache - `meta`: Metadata passed to the query function context. Query Collections may add `loadSubsetOptions` for on-demand queries. -Except for `meta`, these options are only passed to TanStack Query when you define them. If you omit them, `QueryClient.defaultOptions` can still apply. +Additional TanStack Query observer options can be passed through with `queryOptions`: + +```ts +const todosCollection = createCollection( + queryCollectionOptions({ + queryKey: ["todos"], + queryFn: fetchTodos, + queryClient, + getKey: (todo) => todo.id, + queryOptions: { + refetchOnWindowFocus: true, + refetchOnReconnect: true, + refetchOnMount: "always", + networkMode: "online", + }, + }) +) +``` + +Top-level `meta` is always merged by Query Collection so it can add on-demand `loadSubsetOptions`. Other top-level Query options and all `queryOptions` entries are only passed to TanStack Query when you define them. If you omit them, `QueryClient.defaultOptions` can still apply. If the same supported option is provided both at the top level and in `queryOptions`, the existing top-level option wins for backwards compatibility. Some fields are owned or reinterpreted by the collection adapter rather than treated as ordinary Query option pass-through: @@ -81,16 +100,13 @@ Some fields are owned or reinterpreted by the collection adapter rather than tre - `getKey`: Extracts each row's stable TanStack DB key. - Mutation handlers such as `onInsert`, `onUpdate`, and `onDelete`. -Other TanStack Query options are not currently exposed through `queryCollectionOptions`. Common examples include: +Most TanStack Query observer options that are not owned by Query Collection can be provided through `queryOptions`. Adapter-owned fields are intentionally not pass-through options: -- `initialData` -- `placeholderData` -- `refetchOnWindowFocus` -- `refetchOnReconnect` -- `refetchOnMount` -- `networkMode` -- `throwOnError` -- configurable `structuralSharing` +- `queryKey`, `queryFn`, and `queryClient` +- `select` (Query Collection uses this for row extraction, not TanStack Query's observer-level `select` contract) +- `meta` (merged by Query Collection so on-demand `loadSubsetOptions` can be included) +- `subscribed` (Query Collection owns the observer subscription lifecycle) +- `structuralSharing` and `notifyOnChangeProps` (managed by Query Collection synchronization) ### Using with `queryOptions(...)` diff --git a/packages/query-db-collection/src/query.ts b/packages/query-db-collection/src/query.ts index ea303b8ddb..1cab219d2b 100644 --- a/packages/query-db-collection/src/query.ts +++ b/packages/query-db-collection/src/query.ts @@ -49,6 +49,34 @@ type InferSchemaInput = T extends StandardSchemaV1 type TQueryKeyBuilder = (opts: LoadSubsetOptions) => TQueryKey +type QueryCollectionAdapterOwnedQueryOptions = + | `queryKey` + | `queryFn` + | `select` + | `meta` + | `subscribed` + | `structuralSharing` + | `notifyOnChangeProps` + +export type QueryCollectionQueryOptions< + T extends object = object, + TQueryFn extends (context: QueryFunctionContext) => any = ( + context: QueryFunctionContext, + ) => any, + TError = unknown, + TQueryKey extends QueryKey = QueryKey, + TQueryData = Awaited>, +> = Omit< + QueryObserverOptions, TQueryData, TQueryKey>, + QueryCollectionAdapterOwnedQueryOptions +> + +function omitUndefined>(value: T): Partial { + return Object.fromEntries( + Object.entries(value).filter(([, optionValue]) => optionValue !== undefined), + ) as Partial +} + /** * Configuration options for creating a Query Collection * @template T - The explicit type of items stored in the collection @@ -85,6 +113,20 @@ export interface QueryCollectionConfig< /** The TanStack Query client instance */ queryClient: QueryClient + /** + * Additional TanStack Query observer options to pass through. + * Adapter-owned fields such as queryKey, queryFn, select, meta, + * subscribed, structuralSharing, and notifyOnChangeProps are managed by Query Collection. + * Existing top-level Query Collection options override duplicate values here. + */ + queryOptions?: QueryCollectionQueryOptions< + T, + TQueryFn, + TError, + TQueryKey, + TQueryData + > + // Query-specific options /** Whether the query should automatically run (default: true) */ enabled?: QueryObserverOptions< @@ -590,6 +632,7 @@ export function queryCollectionOptions( queryFn, select, queryClient, + queryOptions, enabled, refetchInterval, retry, @@ -1165,6 +1208,17 @@ export function queryCollectionOptions( } } + const { + queryKey: _queryOptionsQueryKey, + queryFn: _queryOptionsQueryFn, + select: _queryOptionsSelect, + meta: _queryOptionsMeta, + subscribed: _queryOptionsSubscribed, + structuralSharing: _queryOptionsStructuralSharing, + notifyOnChangeProps: _queryOptionsNotifyOnChangeProps, + ...passThroughQueryOptions + } = (queryOptions ?? {}) as Record + const observerOptions: QueryObserverOptions< Array, any, @@ -1172,13 +1226,15 @@ export function queryCollectionOptions( Array, any > = { + ...omitUndefined(passThroughQueryOptions), queryKey: key, queryFn: queryFunction, meta: extendedMeta, structuralSharing: true, notifyOnChangeProps: `all`, - // Only include options that are explicitly defined to allow QueryClient defaultOptions to be used + // Only include options that are explicitly defined to allow QueryClient defaultOptions to be used. + // Existing top-level options win over queryOptions for backwards compatibility. ...(enabled !== undefined && { enabled }), ...(refetchInterval !== undefined && { refetchInterval }), ...(retry !== undefined && { retry }), diff --git a/packages/query-db-collection/tests/query.test-d.ts b/packages/query-db-collection/tests/query.test-d.ts index c526398f8a..9d6d23c534 100644 --- a/packages/query-db-collection/tests/query.test-d.ts +++ b/packages/query-db-collection/tests/query.test-d.ts @@ -32,6 +32,46 @@ describe(`Query collection type resolution tests`, () => { // Create a mock QueryClient for tests const queryClient = new QueryClient() + it(`should type supported queryOptions pass-through and reject adapter-owned fields`, () => { + queryCollectionOptions({ + id: `query-options-types`, + queryClient, + queryKey: [`query-options-types`], + queryFn: () => Promise.resolve([]), + getKey: (item) => item.id, + queryOptions: { + refetchOnWindowFocus: true, + refetchOnReconnect: true, + refetchOnMount: `always`, + networkMode: `online`, + }, + }) + + queryCollectionOptions({ + id: `query-options-owned-fields`, + queryClient, + queryKey: [`query-options-owned-fields`], + queryFn: () => Promise.resolve([]), + getKey: (item) => item.id, + queryOptions: { + // @ts-expect-error Query Collection owns row extraction select; it is not a Query pass-through option. + select: (items) => items, + }, + }) + + queryCollectionOptions({ + id: `query-options-subscribed-owned`, + queryClient, + queryKey: [`query-options-subscribed-owned`], + queryFn: () => Promise.resolve([]), + getKey: (item) => item.id, + queryOptions: { + // @ts-expect-error Query Collection owns observer subscription lifecycle. + subscribed: false, + }, + }) + }) + it(`should prioritize explicit type in QueryCollectionConfig`, () => { const options = queryCollectionOptions({ id: `test`, diff --git a/packages/query-db-collection/tests/query.test.ts b/packages/query-db-collection/tests/query.test.ts index 9a792093f4..610dcb79bb 100644 --- a/packages/query-db-collection/tests/query.test.ts +++ b/packages/query-db-collection/tests/query.test.ts @@ -185,6 +185,114 @@ describe(`QueryCollection`, () => { queryClient.clear() }) + it(`should pass through additional Query observer options`, async () => { + const queryKey = [`query-options-pass-through`] + const queryFn = vi.fn().mockResolvedValue([{ id: `1`, name: `Item 1` }]) + + const collection = createCollection( + queryCollectionOptions({ + id: `query-options-pass-through`, + queryClient, + queryKey, + queryFn, + getKey, + startSync: true, + queryOptions: { + refetchOnWindowFocus: true, + refetchOnReconnect: true, + refetchOnMount: `always`, + networkMode: `online`, + }, + }), + ) + + await vi.waitFor(() => { + expect(collection.size).toBe(1) + }) + + const query = queryClient.getQueryCache().find({ queryKey, exact: true }) + const options = query?.options as any + expect(options.refetchOnWindowFocus).toBe(true) + expect(options.refetchOnReconnect).toBe(true) + expect(options.refetchOnMount).toBe(`always`) + expect(options.networkMode).toBe(`online`) + }) + + it(`should ignore adapter-owned subscribed in queryOptions`, async () => { + const queryKey = [`query-options-subscribed-owned`] + const queryFn = vi.fn().mockResolvedValue([{ id: `1`, name: `Item 1` }]) + + const collection = createCollection( + queryCollectionOptions({ + id: `query-options-subscribed-owned`, + queryClient, + queryKey, + queryFn, + getKey, + startSync: true, + queryOptions: { + subscribed: false, + } as any, + }), + ) + + await vi.waitFor(() => { + expect(collection.size).toBe(1) + }) + + const query = queryClient.getQueryCache().find({ queryKey, exact: true }) + const options = query?.options as any + expect(options.subscribed).toBeUndefined() + }) + + it(`should let top-level options override queryOptions and omit undefined values`, async () => { + const queryKey = [`query-options-top-level-precedence`] + const queryFn = vi.fn().mockResolvedValue([{ id: `1`, name: `Item 1` }]) + + const clientWithDefaults = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 1234, + retry: 3, + refetchOnWindowFocus: false, + }, + }, + }) + + const collection = createCollection( + queryCollectionOptions({ + id: `query-options-top-level-precedence`, + queryClient: clientWithDefaults, + queryKey, + queryFn, + getKey, + startSync: true, + staleTime: 5678, + retry: undefined, + queryOptions: { + staleTime: 9999, + retry: undefined, + refetchOnWindowFocus: true, + }, + }), + ) + + await vi.waitFor(() => { + expect(collection.size).toBe(1) + }) + + const query = clientWithDefaults.getQueryCache().find({ + queryKey, + exact: true, + }) + const options = query?.options as any + expect(options.staleTime).toBe(5678) + expect(options.retry).toBe(3) + expect(options.refetchOnWindowFocus).toBe(true) + + clientWithDefaults.clear() + }) + it(`should initialize and fetch initial data`, async () => { const queryKey = [`testItems`] const initialItems: Array = [ From c87c45b3806026b599d94bf62313afe7d6365653 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Fri, 10 Jul 2026 15:03:10 +0000 Subject: [PATCH 2/4] ci: apply automated fixes --- .changeset/query-options-pass-through.md | 2 +- packages/query-db-collection/src/query.ts | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.changeset/query-options-pass-through.md b/.changeset/query-options-pass-through.md index 800936a9cd..9d703cafff 100644 --- a/.changeset/query-options-pass-through.md +++ b/.changeset/query-options-pass-through.md @@ -1,5 +1,5 @@ --- -"@tanstack/query-db-collection": minor +'@tanstack/query-db-collection': minor --- Add `queryOptions` pass-through support for Query Collection observer options while preserving existing top-level option precedence and QueryClient defaultOptions behavior. diff --git a/packages/query-db-collection/src/query.ts b/packages/query-db-collection/src/query.ts index 1cab219d2b..1eda343f18 100644 --- a/packages/query-db-collection/src/query.ts +++ b/packages/query-db-collection/src/query.ts @@ -71,9 +71,13 @@ export type QueryCollectionQueryOptions< QueryCollectionAdapterOwnedQueryOptions > -function omitUndefined>(value: T): Partial { +function omitUndefined>( + value: T, +): Partial { return Object.fromEntries( - Object.entries(value).filter(([, optionValue]) => optionValue !== undefined), + Object.entries(value).filter( + ([, optionValue]) => optionValue !== undefined, + ), ) as Partial } From 4de5e8a8fa8085a64f8144f9f2b3136c8ea20fdc Mon Sep 17 00:00:00 2001 From: Kyle Mathews Date: Fri, 10 Jul 2026 09:50:27 -0600 Subject: [PATCH 3/4] Use flat query option fields --- .changeset/query-options-pass-through.md | 2 +- docs/collections/query-collection.md | 26 ++-- packages/query-db-collection/src/query.ts | 138 ++++++++++-------- .../query-db-collection/tests/query.test-d.ts | 30 +--- .../query-db-collection/tests/query.test.ts | 51 ++----- 5 files changed, 105 insertions(+), 142 deletions(-) diff --git a/.changeset/query-options-pass-through.md b/.changeset/query-options-pass-through.md index 9d703cafff..70a48e43ec 100644 --- a/.changeset/query-options-pass-through.md +++ b/.changeset/query-options-pass-through.md @@ -2,4 +2,4 @@ '@tanstack/query-db-collection': minor --- -Add `queryOptions` pass-through support for Query Collection observer options while preserving existing top-level option precedence and QueryClient defaultOptions behavior. +Add top-level Query Collection support for additional Query observer options while preserving QueryClient defaultOptions behavior. diff --git a/docs/collections/query-collection.md b/docs/collections/query-collection.md index 92c509ecb7..0a0265c391 100644 --- a/docs/collections/query-collection.md +++ b/docs/collections/query-collection.md @@ -56,7 +56,7 @@ The `queryCollectionOptions` function accepts the following options: ### Query Options -Query Collections use TanStack Query internally. Common Query options can be provided either as existing top-level Query Collection options or through the additive `queryOptions` pass-through object. +Query Collections use TanStack Query internally and expose supported Query observer options as top-level `queryCollectionOptions` fields. The following top-level Query Collection options are forwarded to the underlying Query observer: @@ -67,10 +67,12 @@ The following top-level Query Collection options are forwarded to the underlying - `retryDelay`: Delay between retries - `staleTime`: How long data is considered fresh - `gcTime`: How long unused query data stays in the Query cache +- `refetchOnWindowFocus`: Whether to refetch when the window regains focus +- `refetchOnReconnect`: Whether to refetch when the network reconnects +- `refetchOnMount`: Whether to refetch when the observer mounts +- `networkMode`: Query network mode - `meta`: Metadata passed to the query function context. Query Collections may add `loadSubsetOptions` for on-demand queries. -Additional TanStack Query observer options can be passed through with `queryOptions`: - ```ts const todosCollection = createCollection( queryCollectionOptions({ @@ -78,17 +80,15 @@ const todosCollection = createCollection( queryFn: fetchTodos, queryClient, getKey: (todo) => todo.id, - queryOptions: { - refetchOnWindowFocus: true, - refetchOnReconnect: true, - refetchOnMount: "always", - networkMode: "online", - }, + refetchOnWindowFocus: true, + refetchOnReconnect: true, + refetchOnMount: "always", + networkMode: "online", }) ) ``` -Top-level `meta` is always merged by Query Collection so it can add on-demand `loadSubsetOptions`. Other top-level Query options and all `queryOptions` entries are only passed to TanStack Query when you define them. If you omit them, `QueryClient.defaultOptions` can still apply. If the same supported option is provided both at the top level and in `queryOptions`, the existing top-level option wins for backwards compatibility. +Top-level `meta` is always merged by Query Collection so it can add on-demand `loadSubsetOptions`. Other supported top-level Query options are only passed to TanStack Query when you define them. If you omit them, `QueryClient.defaultOptions` can still apply. Some fields are owned or reinterpreted by the collection adapter rather than treated as ordinary Query option pass-through: @@ -100,7 +100,7 @@ Some fields are owned or reinterpreted by the collection adapter rather than tre - `getKey`: Extracts each row's stable TanStack DB key. - Mutation handlers such as `onInsert`, `onUpdate`, and `onDelete`. -Most TanStack Query observer options that are not owned by Query Collection can be provided through `queryOptions`. Adapter-owned fields are intentionally not pass-through options: +Some TanStack Query fields are owned or reinterpreted by Query Collection and are intentionally not exposed as ordinary Query observer options: - `queryKey`, `queryFn`, and `queryClient` - `select` (Query Collection uses this for row extraction, not TanStack Query's observer-level `select` contract) @@ -108,9 +108,11 @@ Most TanStack Query observer options that are not owned by Query Collection can - `subscribed` (Query Collection owns the observer subscription lifecycle) - `structuralSharing` and `notifyOnChangeProps` (managed by Query Collection synchronization) +Other semantic options, such as `initialData`, `placeholderData`, and TanStack Query observer-level `select`, are intentionally deferred until their Query Collection behavior is explicitly designed. + ### Using with `queryOptions(...)` -If your app already uses TanStack Query's `queryOptions` helper (e.g. from `@tanstack/react-query`), you can spread those options into `queryCollectionOptions`. Note that `queryFn` must be explicitly provided since query collections require it both in types and at runtime: +If your app already uses TanStack Query's `queryOptions` helper (e.g. from `@tanstack/react-query`), you can spread compatible top-level options into `queryCollectionOptions`. Note that `queryFn` must be explicitly provided since query collections require it both in types and at runtime, and Query Collection's `select` option is for row extraction rather than TanStack Query observer-level selection: ```typescript import { QueryClient } from "@tanstack/query-core" diff --git a/packages/query-db-collection/src/query.ts b/packages/query-db-collection/src/query.ts index 1eda343f18..c2f7c2168f 100644 --- a/packages/query-db-collection/src/query.ts +++ b/packages/query-db-collection/src/query.ts @@ -49,36 +49,38 @@ type InferSchemaInput = T extends StandardSchemaV1 type TQueryKeyBuilder = (opts: LoadSubsetOptions) => TQueryKey -type QueryCollectionAdapterOwnedQueryOptions = - | `queryKey` - | `queryFn` - | `select` - | `meta` - | `subscribed` - | `structuralSharing` - | `notifyOnChangeProps` - -export type QueryCollectionQueryOptions< - T extends object = object, - TQueryFn extends (context: QueryFunctionContext) => any = ( - context: QueryFunctionContext, - ) => any, - TError = unknown, - TQueryKey extends QueryKey = QueryKey, - TQueryData = Awaited>, -> = Omit< - QueryObserverOptions, TQueryData, TQueryKey>, - QueryCollectionAdapterOwnedQueryOptions +const queryObserverOptionKeys = [ + `enabled`, + `refetchInterval`, + `retry`, + `retryDelay`, + `staleTime`, + `gcTime`, + `refetchOnWindowFocus`, + `refetchOnReconnect`, + `refetchOnMount`, + `networkMode`, +] as const + +type QueryObserverOptionKey = (typeof queryObserverOptionKeys)[number] + +type QueryObserverOptionValues = Pick< + QueryObserverOptions, any, Array, Array, any>, + QueryObserverOptionKey > -function omitUndefined>( - value: T, -): Partial { - return Object.fromEntries( - Object.entries(value).filter( - ([, optionValue]) => optionValue !== undefined, - ), - ) as Partial +function pickDefinedQueryObserverOptions( + config: Partial, +): Partial { + const options: Partial = {} + + for (const key of queryObserverOptionKeys) { + if (config[key] !== undefined) { + ;(options as Record)[key] = config[key] + } + } + + return options } /** @@ -117,20 +119,6 @@ export interface QueryCollectionConfig< /** The TanStack Query client instance */ queryClient: QueryClient - /** - * Additional TanStack Query observer options to pass through. - * Adapter-owned fields such as queryKey, queryFn, select, meta, - * subscribed, structuralSharing, and notifyOnChangeProps are managed by Query Collection. - * Existing top-level Query Collection options override duplicate values here. - */ - queryOptions?: QueryCollectionQueryOptions< - T, - TQueryFn, - TError, - TQueryKey, - TQueryData - > - // Query-specific options /** Whether the query should automatically run (default: true) */ enabled?: QueryObserverOptions< @@ -175,6 +163,34 @@ export interface QueryCollectionConfig< TQueryData, TQueryKey >[`gcTime`] + refetchOnWindowFocus?: QueryObserverOptions< + TQueryData, + TError, + Array, + TQueryData, + TQueryKey + >[`refetchOnWindowFocus`] + refetchOnReconnect?: QueryObserverOptions< + TQueryData, + TError, + Array, + TQueryData, + TQueryKey + >[`refetchOnReconnect`] + refetchOnMount?: QueryObserverOptions< + TQueryData, + TError, + Array, + TQueryData, + TQueryKey + >[`refetchOnMount`] + networkMode?: QueryObserverOptions< + TQueryData, + TError, + Array, + TQueryData, + TQueryKey + >[`networkMode`] persistedGcTime?: number /** @@ -636,13 +652,16 @@ export function queryCollectionOptions( queryFn, select, queryClient, - queryOptions, enabled, refetchInterval, retry, retryDelay, staleTime, gcTime, + refetchOnWindowFocus, + refetchOnReconnect, + refetchOnMount, + networkMode, persistedGcTime, getKey, onInsert, @@ -1212,17 +1231,6 @@ export function queryCollectionOptions( } } - const { - queryKey: _queryOptionsQueryKey, - queryFn: _queryOptionsQueryFn, - select: _queryOptionsSelect, - meta: _queryOptionsMeta, - subscribed: _queryOptionsSubscribed, - structuralSharing: _queryOptionsStructuralSharing, - notifyOnChangeProps: _queryOptionsNotifyOnChangeProps, - ...passThroughQueryOptions - } = (queryOptions ?? {}) as Record - const observerOptions: QueryObserverOptions< Array, any, @@ -1230,21 +1238,23 @@ export function queryCollectionOptions( Array, any > = { - ...omitUndefined(passThroughQueryOptions), + ...pickDefinedQueryObserverOptions({ + enabled, + refetchInterval, + retry, + retryDelay, + staleTime, + gcTime, + refetchOnWindowFocus, + refetchOnReconnect, + refetchOnMount, + networkMode, + }), queryKey: key, queryFn: queryFunction, meta: extendedMeta, structuralSharing: true, notifyOnChangeProps: `all`, - - // Only include options that are explicitly defined to allow QueryClient defaultOptions to be used. - // Existing top-level options win over queryOptions for backwards compatibility. - ...(enabled !== undefined && { enabled }), - ...(refetchInterval !== undefined && { refetchInterval }), - ...(retry !== undefined && { retry }), - ...(retryDelay !== undefined && { retryDelay }), - ...(staleTime !== undefined && { staleTime }), - ...(gcTime !== undefined && { gcTime }), } const localObserver = new QueryObserver< diff --git a/packages/query-db-collection/tests/query.test-d.ts b/packages/query-db-collection/tests/query.test-d.ts index 9d6d23c534..30e8f64c9c 100644 --- a/packages/query-db-collection/tests/query.test-d.ts +++ b/packages/query-db-collection/tests/query.test-d.ts @@ -32,31 +32,17 @@ describe(`Query collection type resolution tests`, () => { // Create a mock QueryClient for tests const queryClient = new QueryClient() - it(`should type supported queryOptions pass-through and reject adapter-owned fields`, () => { + it(`should type supported top-level Query observer options and reject adapter-owned fields`, () => { queryCollectionOptions({ id: `query-options-types`, queryClient, queryKey: [`query-options-types`], queryFn: () => Promise.resolve([]), getKey: (item) => item.id, - queryOptions: { - refetchOnWindowFocus: true, - refetchOnReconnect: true, - refetchOnMount: `always`, - networkMode: `online`, - }, - }) - - queryCollectionOptions({ - id: `query-options-owned-fields`, - queryClient, - queryKey: [`query-options-owned-fields`], - queryFn: () => Promise.resolve([]), - getKey: (item) => item.id, - queryOptions: { - // @ts-expect-error Query Collection owns row extraction select; it is not a Query pass-through option. - select: (items) => items, - }, + refetchOnWindowFocus: true, + refetchOnReconnect: true, + refetchOnMount: `always`, + networkMode: `online`, }) queryCollectionOptions({ @@ -65,10 +51,8 @@ describe(`Query collection type resolution tests`, () => { queryKey: [`query-options-subscribed-owned`], queryFn: () => Promise.resolve([]), getKey: (item) => item.id, - queryOptions: { - // @ts-expect-error Query Collection owns observer subscription lifecycle. - subscribed: false, - }, + // @ts-expect-error Query Collection owns observer subscription lifecycle. + subscribed: false, }) }) diff --git a/packages/query-db-collection/tests/query.test.ts b/packages/query-db-collection/tests/query.test.ts index 610dcb79bb..ff516d28eb 100644 --- a/packages/query-db-collection/tests/query.test.ts +++ b/packages/query-db-collection/tests/query.test.ts @@ -185,7 +185,7 @@ describe(`QueryCollection`, () => { queryClient.clear() }) - it(`should pass through additional Query observer options`, async () => { + it(`should pass through additional top-level Query observer options`, async () => { const queryKey = [`query-options-pass-through`] const queryFn = vi.fn().mockResolvedValue([{ id: `1`, name: `Item 1` }]) @@ -197,12 +197,10 @@ describe(`QueryCollection`, () => { queryFn, getKey, startSync: true, - queryOptions: { - refetchOnWindowFocus: true, - refetchOnReconnect: true, - refetchOnMount: `always`, - networkMode: `online`, - }, + refetchOnWindowFocus: true, + refetchOnReconnect: true, + refetchOnMount: `always`, + networkMode: `online`, }), ) @@ -218,35 +216,8 @@ describe(`QueryCollection`, () => { expect(options.networkMode).toBe(`online`) }) - it(`should ignore adapter-owned subscribed in queryOptions`, async () => { - const queryKey = [`query-options-subscribed-owned`] - const queryFn = vi.fn().mockResolvedValue([{ id: `1`, name: `Item 1` }]) - - const collection = createCollection( - queryCollectionOptions({ - id: `query-options-subscribed-owned`, - queryClient, - queryKey, - queryFn, - getKey, - startSync: true, - queryOptions: { - subscribed: false, - } as any, - }), - ) - - await vi.waitFor(() => { - expect(collection.size).toBe(1) - }) - - const query = queryClient.getQueryCache().find({ queryKey, exact: true }) - const options = query?.options as any - expect(options.subscribed).toBeUndefined() - }) - - it(`should let top-level options override queryOptions and omit undefined values`, async () => { - const queryKey = [`query-options-top-level-precedence`] + it(`should omit undefined Query observer options to preserve defaults`, async () => { + const queryKey = [`query-options-default-preservation`] const queryFn = vi.fn().mockResolvedValue([{ id: `1`, name: `Item 1` }]) const clientWithDefaults = new QueryClient({ @@ -261,7 +232,7 @@ describe(`QueryCollection`, () => { const collection = createCollection( queryCollectionOptions({ - id: `query-options-top-level-precedence`, + id: `query-options-default-preservation`, queryClient: clientWithDefaults, queryKey, queryFn, @@ -269,11 +240,7 @@ describe(`QueryCollection`, () => { startSync: true, staleTime: 5678, retry: undefined, - queryOptions: { - staleTime: 9999, - retry: undefined, - refetchOnWindowFocus: true, - }, + refetchOnWindowFocus: true, }), ) From 348d4f794a93db1eae9a3301dcba879e49896407 Mon Sep 17 00:00:00 2001 From: Kyle Mathews Date: Fri, 10 Jul 2026 15:53:59 -0600 Subject: [PATCH 4/4] Mount query client during collection sync --- packages/query-db-collection/src/query.ts | 40 ++++++++++++- .../query-db-collection/tests/query.test.ts | 56 ++++++++++++++++++- 2 files changed, 93 insertions(+), 3 deletions(-) diff --git a/packages/query-db-collection/src/query.ts b/packages/query-db-collection/src/query.ts index c2f7c2168f..59c8519678 100644 --- a/packages/query-db-collection/src/query.ts +++ b/packages/query-db-collection/src/query.ts @@ -1946,6 +1946,23 @@ export function queryCollectionOptions( // Enhanced internalSync that captures write functions for manual use const enhancedInternalSync: SyncConfig[`sync`] = (params) => { const { begin, write, commit, collection } = params + let queryClientMounted = false + + const mountQueryClient = () => { + if (!queryClientMounted) { + queryClient.mount() + queryClientMounted = true + } + } + + const unmountQueryClient = () => { + if (queryClientMounted) { + queryClient.unmount() + queryClientMounted = false + } + } + + mountQueryClient() // Get the base query key for the context (handle both static and function-based keys) const contextQueryKey = @@ -1965,8 +1982,27 @@ export function queryCollectionOptions( updateCacheData, } - // Call the original internalSync logic - return internalSync(params) + // Call the original internalSync logic, pairing QueryClient mount with the + // collection sync lifecycle so focus/reconnect managers dispatch events for + // standalone QueryClient usage. + const syncResult = internalSync(params) + const sync = + typeof syncResult === `function` + ? { cleanup: syncResult } + : typeof syncResult === `object` + ? syncResult + : {} + + return { + ...sync, + cleanup: () => { + try { + sync.cleanup?.() + } finally { + unmountQueryClient() + } + }, + } } // Create write utils using the manual-sync module diff --git a/packages/query-db-collection/tests/query.test.ts b/packages/query-db-collection/tests/query.test.ts index ff516d28eb..c711d661f7 100644 --- a/packages/query-db-collection/tests/query.test.ts +++ b/packages/query-db-collection/tests/query.test.ts @@ -1,5 +1,11 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { QueryClient, dehydrate, hashKey } from '@tanstack/query-core' +import { + QueryClient, + dehydrate, + focusManager, + hashKey, + onlineManager, +} from '@tanstack/query-core' import { BTreeIndex, createCollection, @@ -216,6 +222,54 @@ describe(`QueryCollection`, () => { expect(options.networkMode).toBe(`online`) }) + it(`should refetch on focus and reconnect with standalone QueryClient`, async () => { + const queryKey = [`query-options-event-refetch`] + const queryFn = vi + .fn() + .mockResolvedValueOnce([{ id: `1`, name: `Initial` }]) + .mockResolvedValueOnce([{ id: `1`, name: `Focused` }]) + .mockResolvedValueOnce([{ id: `1`, name: `Reconnected` }]) + + const collection = createCollection( + queryCollectionOptions({ + id: `query-options-event-refetch`, + queryClient, + queryKey, + queryFn, + getKey, + startSync: true, + staleTime: 0, + refetchOnWindowFocus: true, + refetchOnReconnect: true, + }), + ) + + try { + await vi.waitFor(() => { + expect(collection.size).toBe(1) + expect(queryFn).toHaveBeenCalledTimes(1) + }) + + focusManager.setFocused(false) + focusManager.setFocused(true) + + await vi.waitFor(() => { + expect(queryFn).toHaveBeenCalledTimes(2) + }) + + onlineManager.setOnline(false) + onlineManager.setOnline(true) + + await vi.waitFor(() => { + expect(queryFn).toHaveBeenCalledTimes(3) + }) + } finally { + await collection.cleanup() + focusManager.setFocused(undefined) + onlineManager.setOnline(true) + } + }) + it(`should omit undefined Query observer options to preserve defaults`, async () => { const queryKey = [`query-options-default-preservation`] const queryFn = vi.fn().mockResolvedValue([{ id: `1`, name: `Item 1` }])