From 9add1ec12f798157a94d2afd99de0ead514b700a Mon Sep 17 00:00:00 2001 From: Kyle Mathews Date: Fri, 21 Aug 2026 10:39:09 -0600 Subject: [PATCH 1/4] fix: support conditional live query configs --- docs/guides/live-queries.md | 19 ++++- packages/db/src/live-query-options.ts | 30 +++++-- packages/db/tests/live-query-options.test.ts | 16 ++++ packages/react-db/skills/react-db/SKILL.md | 12 +-- packages/react-db/src/useLiveQuery.ts | 80 +++++++++++++++---- packages/react-db/src/useLiveSuspenseQuery.ts | 4 +- .../react-db/tests/useLiveQuery.test-d.tsx | 56 +++++++++++++ packages/react-db/tests/useLiveQuery.test.tsx | 71 +++++++++++++++- 8 files changed, 254 insertions(+), 34 deletions(-) diff --git a/docs/guides/live-queries.md b/docs/guides/live-queries.md index eb2bf08f0..2fcc57714 100644 --- a/docs/guides/live-queries.md +++ b/docs/guides/live-queries.md @@ -454,11 +454,26 @@ function TodoList({ userId }: { userId: string }) { } ``` -The callback form can also return `undefined` or `null` to disable a query. This still uses derived identity, so captured structured values do not need a dependency array. When the query is disabled: +The `query` callback can return `undefined` or `null` to disable a query. This still uses derived identity, so captured structured values do not need a dependency array: + +```tsx +const { data, isEnabled, status } = useLiveQuery({ + query: (q) => { + if (!userId) return undefined + + return q + .from({ todos: todosCollection }) + .where(({ todos }) => eq(todos.userId, userId)) + }, +}) +``` + +The top-level callback form supports the same behavior. When the query is disabled: - `status` is `'disabled'` - `data`, `state`, and `collection` are `undefined` - `isEnabled` is `false` -- `isLoading`, `isReady`, `isIdle`, and `isError` are all `false` +- `isReady` is `true` +- `isLoading`, `isIdle`, `isError`, and `isCleanedUp` are all `false` ### Alternative Input Forms diff --git a/packages/db/src/live-query-options.ts b/packages/db/src/live-query-options.ts index 4005c3f9b..7439b2f14 100644 --- a/packages/db/src/live-query-options.ts +++ b/packages/db/src/live-query-options.ts @@ -8,8 +8,10 @@ import type { CollectionImpl } from './collection/index.js' import type { CollectionOptionsIdentity } from './collection-options.js' import type { CollectionOptions, DbClient } from './client.js' import type { + Context, InitialQueryBuilder, LiveQueryCollectionConfig, + QueryBuilder, } from './query/index.js' export type LiveQueryKey = ReadonlyArray @@ -22,6 +24,17 @@ export type DeferredLiveQueryCollections = Set< CollectionImpl > +type PreparedLiveQueryConfigInput = Omit< + LiveQueryCollectionConfig, + `query` +> & { + query: + | QueryBuilder + | ((q: InitialQueryBuilder) => QueryBuilder | undefined | null) + queryKey?: LiveQueryKey + client?: DbClient +} + function createInitialQueryBuilder( dbClient: DbClient | undefined, deferredCollections: DeferredLiveQueryCollections, @@ -73,17 +86,20 @@ export function prepareLiveQueryValue( queryKey: _queryKey, client: _client, ...config - } = value as LiveQueryCollectionConfig & { - queryKey?: LiveQueryKey - client?: DbClient + } = value as PreparedLiveQueryConfigInput + + const preparedQuery = + typeof query === `function` + ? query(createInitialQueryBuilder(dbClient, deferredCollections)) + : query + + if (preparedQuery === undefined || preparedQuery === null) { + return preparedQuery } return { ...config, - query: - typeof query === `function` - ? query(createInitialQueryBuilder(dbClient, deferredCollections)) - : query, + query: preparedQuery, } } diff --git a/packages/db/tests/live-query-options.test.ts b/packages/db/tests/live-query-options.test.ts index 5283d0cff..6c2500d42 100644 --- a/packages/db/tests/live-query-options.test.ts +++ b/packages/db/tests/live-query-options.test.ts @@ -3,9 +3,25 @@ import { createCollection } from '../src/collection/index.js' import { getLiveQueryHash, getPreparedLiveQueryIdentity, + prepareLiveQueryValue, } from '../src/live-query-options.js' import { BaseQueryBuilder } from '../src/query/builder/index.js' +describe(`live query preparation`, () => { + it.each([undefined, null])( + `promotes a nullish config query result to a disabled query`, + (disabled) => { + const prepared = prepareLiveQueryValue( + { query: () => disabled }, + undefined, + new Set(), + ) + + expect(prepared).toBe(disabled) + }, + ) +}) + describe(`live query identity`, () => { it(`hashes Map values in an explicit queryKey deterministically`, () => { const first = getLiveQueryHash(undefined, [ diff --git a/packages/react-db/skills/react-db/SKILL.md b/packages/react-db/skills/react-db/SKILL.md index 90cfbc7bb..273fad8cb 100644 --- a/packages/react-db/skills/react-db/SKILL.md +++ b/packages/react-db/skills/react-db/SKILL.md @@ -89,11 +89,13 @@ const { data } = useLiveQuery({ const { data } = useLiveQuery(preloadedCollection) // Conditional query — derived identity handles enabled/disabled transitions -const { data, status } = useLiveQuery((q) => { - if (!userId) return undefined - return q - .from({ todo: todoCollection }) - .where(({ todo }) => eq(todo.userId, userId)) +const { data, status } = useLiveQuery({ + query: (q) => { + if (!userId) return undefined + return q + .from({ todo: todoCollection }) + .where(({ todo }) => eq(todo.userId, userId)) + }, }) // When disabled: status='disabled', data=undefined ``` diff --git a/packages/react-db/src/useLiveQuery.ts b/packages/react-db/src/useLiveQuery.ts index 7f8e303dd..a42f6c874 100644 --- a/packages/react-db/src/useLiveQuery.ts +++ b/packages/react-db/src/useLiveQuery.ts @@ -48,16 +48,36 @@ export type DerivedIdentityProfiler = { export type UseLiveQueryStatus = CollectionStatus | `disabled` export type LiveQueryKey = ReadonlyArray +type UseLiveQueryConfigOptions = Omit< + LiveQueryCollectionConfig, + `query` +> & { + /** + * Explicit identity for queries that contain opaque functional variants or + * are hot enough that deriving identity from structured IR is too expensive. + * Structured queries should omit this so DB can derive identity directly. + */ + queryKey?: LiveQueryKey + /** Override the nearest DbProvider for this query. */ + client?: DbClient +} + +type ConfiguredQueryBuilder = Extract< + LiveQueryCollectionConfig[`query`], + QueryBuilder +> + +export type EnabledUseLiveQueryConfig = + UseLiveQueryConfigOptions & + Pick, `query`> + export type UseLiveQueryConfig = - LiveQueryCollectionConfig & { - /** - * Explicit identity for queries that contain opaque functional variants or - * are hot enough that deriving identity from structured IR is too expensive. - * Structured queries should omit this so DB can derive identity directly. - */ - queryKey?: LiveQueryKey - /** Override the nearest DbProvider for this query. */ - client?: DbClient + UseLiveQueryConfigOptions & { + query: + | ConfiguredQueryBuilder + | (( + q: InitialQueryBuilder, + ) => ConfiguredQueryBuilder | undefined | null) } export function warnDeprecatedDepsArray( @@ -297,6 +317,16 @@ function createCollectionFromPreparedQuery(value: unknown) { * }) * * @example + * // Return undefined or null to disable a query + * const { data, isEnabled } = useLiveQuery({ + * query: (q) => { + * if (!userId) return undefined + * return q.from({ todos: todosCollection }) + * .where(({ todos }) => eq(todos.userId, userId)) + * }, + * }) + * + * @example * // Join pattern * const { data } = useLiveQuery({ * query: (q) => @@ -476,21 +506,37 @@ export function useLiveQuery< */ // Overload 6: Accept config object export function useLiveQuery( - config: UseLiveQueryConfig, + config: EnabledUseLiveQueryConfig, ): { state: Map> data: InferResultType collection: Collection, string | number, {}> - status: CollectionStatus // Can't be disabled for config objects + status: CollectionStatus // Can't be disabled when query always returns a builder isLoading: boolean isReady: boolean isIdle: boolean isError: boolean isCleanedUp: boolean - isEnabled: true // Always true for config objects + isEnabled: true // Always true when query always returns a builder +} + +// Overload 7: Accept config object with a query that can return undefined/null +export function useLiveQuery( + config: UseLiveQueryConfig, +): { + state: Map> | undefined + data: InferResultType | undefined + collection: Collection, string | number, {}> | undefined + status: UseLiveQueryStatus + isLoading: boolean + isReady: boolean + isIdle: boolean + isError: boolean + isCleanedUp: boolean + isEnabled: boolean } -// Overload 7: Accept config object with legacy deps +// Overload 8: Accept config object with legacy deps export function useLiveQuery( config: LiveQueryCollectionConfig, deps?: Array, @@ -498,13 +544,13 @@ export function useLiveQuery( state: Map> data: InferResultType collection: Collection, string | number, {}> - status: CollectionStatus // Can't be disabled for config objects + status: CollectionStatus // Can't be disabled when query always returns a builder isLoading: boolean isReady: boolean isIdle: boolean isError: boolean isCleanedUp: boolean - isEnabled: true // Always true for config objects + isEnabled: true // Always true when query always returns a builder } /** @@ -536,7 +582,7 @@ export function useLiveQuery( * * return
{data.map(item => )}
*/ -// Overload 8: Accept pre-created live query collection +// Overload 9: Accept pre-created live query collection export function useLiveQuery< TResult extends object, TKey extends string | number, @@ -556,7 +602,7 @@ export function useLiveQuery< isEnabled: true // Always true for pre-created live query collections } -// Overload 9: Accept pre-created live query collection with singleResult: true +// Overload 10: Accept pre-created live query collection with singleResult: true export function useLiveQuery< TResult extends object, TKey extends string | number, diff --git a/packages/react-db/src/useLiveSuspenseQuery.ts b/packages/react-db/src/useLiveSuspenseQuery.ts index a4e8dec5b..7c8152073 100644 --- a/packages/react-db/src/useLiveSuspenseQuery.ts +++ b/packages/react-db/src/useLiveSuspenseQuery.ts @@ -3,7 +3,7 @@ import { useRef } from 'react' import { useLiveQuery } from './useLiveQuery' import { getLiveQueryResultInfo } from './live-query-internals' -import type { UseLiveQueryConfig } from './useLiveQuery' +import type { EnabledUseLiveQueryConfig } from './useLiveQuery' import type { Collection, Context, @@ -118,7 +118,7 @@ export function useLiveSuspenseQuery( // Overload 2: Accept config object export function useLiveSuspenseQuery( - config: UseLiveQueryConfig, + config: EnabledUseLiveQueryConfig, ): { state: Map> data: InferResultType diff --git a/packages/react-db/tests/useLiveQuery.test-d.tsx b/packages/react-db/tests/useLiveQuery.test-d.tsx index 7d31205fd..1b144b8a0 100644 --- a/packages/react-db/tests/useLiveQuery.test-d.tsx +++ b/packages/react-db/tests/useLiveQuery.test-d.tsx @@ -4,6 +4,7 @@ import { createCollection } from '../../db/src/collection/index' import { collectionOptions } from '../../db/src/index' import { mockSyncCollectionOptions } from '../../db/tests/utils' import { + Query, createLiveQueryCollection, eq, liveQueryCollectionOptions, @@ -17,6 +18,11 @@ import type { DbClient, DehydratedDbState } from '../../db/src/index' import type { JSX } from 'react' import type { OutputWithVirtual } from '../../db/tests/utils' import type { SingleResult } from '../../db/src/types' +import type { QueryBuilder } from '../../db/src/query/index' +import type { + UseLiveQueryConfig, + UseLiveQueryStatus, +} from '../src/useLiveQuery' type Person = { id: string @@ -91,6 +97,56 @@ describe(`useLiveQuery type assertions`, () => { >() }) + it(`types a conditional findOne config object as disabled-capable`, () => { + const collection = createCollection( + mockSyncCollectionOptions({ + id: `test-conditional-person-config`, + getKey: (person: Person) => person.id, + initialData: [], + }), + ) + const enabled = null as unknown as boolean + const query = new Query() + .from({ collection }) + .where(({ collection: c }) => eq(c.id, `3`)) + .findOne() + type QueryContext = + typeof query extends QueryBuilder ? TContext : never + const config: UseLiveQueryConfig = { + queryKey: [collection.id, enabled], + query: () => (enabled ? query : undefined), + } + + const { result } = renderHook(() => { + return useLiveQuery(config) + }) + + expectTypeOf(result.current.data).toMatchTypeOf< + OutputWithVirtual | undefined + >() + expectTypeOf(result.current.status).toEqualTypeOf() + expectTypeOf(result.current.isEnabled).toEqualTypeOf() + }) + + it(`rejects a conditional config with a top-level scalar result`, () => { + const collection = createCollection( + mockSyncCollectionOptions({ + id: `test-conditional-scalar-config`, + getKey: (person: Person) => person.id, + initialData: [], + }), + ) + const enabled = null as unknown as boolean + + useLiveQuery({ + // @ts-expect-error - top-level scalar results are not supported + query: (q) => { + if (!enabled) return undefined + return q.from({ collection }).select(({ collection: c }) => c.name) + }, + }) + }) + it(`should type config object to return query rows without queryKey`, () => { const collection = createCollection( mockSyncCollectionOptions({ diff --git a/packages/react-db/tests/useLiveQuery.test.tsx b/packages/react-db/tests/useLiveQuery.test.tsx index 149aef9ee..0ba4587c8 100644 --- a/packages/react-db/tests/useLiveQuery.test.tsx +++ b/packages/react-db/tests/useLiveQuery.test.tsx @@ -1981,7 +1981,76 @@ describe(`Query Collections`, () => { }) }) - describe(`callback variants with conditional returns`, () => { + describe(`conditional returns`, () => { + it(`disables a config query that returns undefined`, async () => { + const collection = createCollection( + mockSyncCollectionOptions({ + id: `undefined-config-query-test`, + getKey: (person: Person) => person.id, + initialData: initialPersons, + }), + ) + + const { result, rerender } = renderHook( + ({ enabled }: { enabled: boolean }) => + useLiveQuery({ + query: (q) => { + if (!enabled) return undefined + return q + .from({ persons: collection }) + .where(({ persons }) => gt(persons.age, 30)) + }, + }), + { initialProps: { enabled: false } }, + ) + + expect(result.current.data).toBeUndefined() + expect(result.current.collection).toBeUndefined() + expect(result.current.status).toBe(`disabled`) + expect(result.current.isEnabled).toBe(false) + expect(result.current.isReady).toBe(true) + + rerender({ enabled: true }) + + await waitFor(() => expect(result.current.data).toHaveLength(1)) + expect(result.current.status).toBe(`ready`) + expect(result.current.isEnabled).toBe(true) + + rerender({ enabled: false }) + + expect(result.current.data).toBeUndefined() + expect(result.current.collection).toBeUndefined() + expect(result.current.status).toBe(`disabled`) + expect(result.current.isEnabled).toBe(false) + expect(result.current.isReady).toBe(true) + }) + + it(`disables a config query that returns null`, () => { + const collection = createCollection( + mockSyncCollectionOptions({ + id: `null-config-query-test`, + getKey: (person: Person) => person.id, + initialData: initialPersons, + }), + ) + + const { result } = renderHook( + ({ enabled }: { enabled: boolean }) => + useLiveQuery({ + query: (q) => { + if (!enabled) return null + return q.from({ persons: collection }) + }, + }), + { initialProps: { enabled: false } }, + ) + + expect(result.current.data).toBeUndefined() + expect(result.current.collection).toBeUndefined() + expect(result.current.status).toBe(`disabled`) + expect(result.current.isEnabled).toBe(false) + }) + it(`should handle callback returning undefined without a dependency array`, async () => { const collection = createCollection( mockSyncCollectionOptions({ From 7cfd1b84b734dc42c9e742fcde78ba8aff169450 Mon Sep 17 00:00:00 2001 From: Kyle Mathews Date: Fri, 21 Aug 2026 10:41:02 -0600 Subject: [PATCH 2/4] docs: add conditional live query changeset --- .changeset/support-conditional-live-query-configs.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/support-conditional-live-query-configs.md diff --git a/.changeset/support-conditional-live-query-configs.md b/.changeset/support-conditional-live-query-configs.md new file mode 100644 index 000000000..656df1b0d --- /dev/null +++ b/.changeset/support-conditional-live-query-configs.md @@ -0,0 +1,6 @@ +--- +'@tanstack/db': patch +'@tanstack/react-db': patch +--- + +Support disabling live queries declared with the `{ query }` config syntax by returning `undefined` or `null` from the query callback. From 92e77e72788ceb1b668ac2de75fe8d9ae6046e05 Mon Sep 17 00:00:00 2001 From: Kyle Mathews Date: Fri, 21 Aug 2026 12:37:01 -0600 Subject: [PATCH 3/4] fix: preserve live query config compatibility --- packages/react-db/src/index.ts | 1 + packages/react-db/src/useLiveQuery.ts | 27 ++++++++-- packages/react-db/src/useLiveSuspenseQuery.ts | 4 +- .../react-db/tests/useLiveQuery.test-d.tsx | 54 ++++++++++++++++++- packages/react-db/tests/useLiveQuery.test.tsx | 42 +++++++++++++++ 5 files changed, 119 insertions(+), 9 deletions(-) diff --git a/packages/react-db/src/index.ts b/packages/react-db/src/index.ts index b042d41c4..b616297a8 100644 --- a/packages/react-db/src/index.ts +++ b/packages/react-db/src/index.ts @@ -1,6 +1,7 @@ // Re-export all public APIs export { useLiveQuery } from './useLiveQuery' export type { + ConditionalUseLiveQueryConfig, LiveQueryKey, UseLiveQueryConfig, UseLiveQueryStatus, diff --git a/packages/react-db/src/useLiveQuery.ts b/packages/react-db/src/useLiveQuery.ts index a42f6c874..c9c012646 100644 --- a/packages/react-db/src/useLiveQuery.ts +++ b/packages/react-db/src/useLiveQuery.ts @@ -67,11 +67,11 @@ type ConfiguredQueryBuilder = Extract< QueryBuilder > -export type EnabledUseLiveQueryConfig = +export type UseLiveQueryConfig = UseLiveQueryConfigOptions & Pick, `query`> -export type UseLiveQueryConfig = +export type ConditionalUseLiveQueryConfig = UseLiveQueryConfigOptions & { query: | ConfiguredQueryBuilder @@ -506,7 +506,7 @@ export function useLiveQuery< */ // Overload 6: Accept config object export function useLiveQuery( - config: EnabledUseLiveQueryConfig, + config: UseLiveQueryConfig, ): { state: Map> data: InferResultType @@ -522,7 +522,7 @@ export function useLiveQuery( // Overload 7: Accept config object with a query that can return undefined/null export function useLiveQuery( - config: UseLiveQueryConfig, + config: ConditionalUseLiveQueryConfig, ): { state: Map> | undefined data: InferResultType | undefined @@ -553,6 +553,23 @@ export function useLiveQuery( isEnabled: true // Always true when query always returns a builder } +// Overload 9: Accept config object with legacy deps and a query that can return undefined/null +export function useLiveQuery( + config: ConditionalUseLiveQueryConfig, + deps: Array, +): { + state: Map> | undefined + data: InferResultType | undefined + collection: Collection, string | number, {}> | undefined + status: UseLiveQueryStatus + isLoading: boolean + isReady: boolean + isIdle: boolean + isError: boolean + isCleanedUp: boolean + isEnabled: boolean +} + /** * Subscribe to an existing live query collection * @param liveQueryCollection - Pre-created live query collection to subscribe to @@ -582,7 +599,7 @@ export function useLiveQuery( * * return
{data.map(item => )}
*/ -// Overload 9: Accept pre-created live query collection +// Overload 10: Accept pre-created live query collection export function useLiveQuery< TResult extends object, TKey extends string | number, diff --git a/packages/react-db/src/useLiveSuspenseQuery.ts b/packages/react-db/src/useLiveSuspenseQuery.ts index 7c8152073..a4e8dec5b 100644 --- a/packages/react-db/src/useLiveSuspenseQuery.ts +++ b/packages/react-db/src/useLiveSuspenseQuery.ts @@ -3,7 +3,7 @@ import { useRef } from 'react' import { useLiveQuery } from './useLiveQuery' import { getLiveQueryResultInfo } from './live-query-internals' -import type { EnabledUseLiveQueryConfig } from './useLiveQuery' +import type { UseLiveQueryConfig } from './useLiveQuery' import type { Collection, Context, @@ -118,7 +118,7 @@ export function useLiveSuspenseQuery( // Overload 2: Accept config object export function useLiveSuspenseQuery( - config: EnabledUseLiveQueryConfig, + config: UseLiveQueryConfig, ): { state: Map> data: InferResultType diff --git a/packages/react-db/tests/useLiveQuery.test-d.tsx b/packages/react-db/tests/useLiveQuery.test-d.tsx index 1b144b8a0..c541e71e7 100644 --- a/packages/react-db/tests/useLiveQuery.test-d.tsx +++ b/packages/react-db/tests/useLiveQuery.test-d.tsx @@ -20,9 +20,10 @@ import type { OutputWithVirtual } from '../../db/tests/utils' import type { SingleResult } from '../../db/src/types' import type { QueryBuilder } from '../../db/src/query/index' import type { + ConditionalUseLiveQueryConfig, UseLiveQueryConfig, UseLiveQueryStatus, -} from '../src/useLiveQuery' +} from '../src/index' type Person = { id: string @@ -112,7 +113,7 @@ describe(`useLiveQuery type assertions`, () => { .findOne() type QueryContext = typeof query extends QueryBuilder ? TContext : never - const config: UseLiveQueryConfig = { + const config: ConditionalUseLiveQueryConfig = { queryKey: [collection.id, enabled], query: () => (enabled ? query : undefined), } @@ -128,6 +129,55 @@ describe(`useLiveQuery type assertions`, () => { expectTypeOf(result.current.isEnabled).toEqualTypeOf() }) + it(`accepts an annotated enabled config in useLiveSuspenseQuery`, () => { + const collection = createCollection( + mockSyncCollectionOptions({ + id: `test-annotated-suspense-config`, + getKey: (person: Person) => person.id, + initialData: [], + }), + ) + const query = new Query().from({ collection }) + type QueryContext = + typeof query extends QueryBuilder ? TContext : never + const config: UseLiveQueryConfig = { + query: () => query, + } + + const { result } = renderHook(() => useLiveSuspenseQuery(config)) + + expectTypeOf(result.current.data).toMatchTypeOf< + Array> + >() + }) + + it(`types a conditional config with deprecated dependencies`, () => { + const collection = createCollection( + mockSyncCollectionOptions({ + id: `test-conditional-config-deps`, + getKey: (person: Person) => person.id, + initialData: [], + }), + ) + const enabled = null as unknown as boolean + + const { result } = renderHook(() => + useLiveQuery( + { + query: (q) => + enabled ? q.from({ collection }).findOne() : undefined, + }, + [enabled], + ), + ) + + expectTypeOf(result.current.data).toMatchTypeOf< + OutputWithVirtual | undefined + >() + expectTypeOf(result.current.status).toEqualTypeOf() + expectTypeOf(result.current.isEnabled).toEqualTypeOf() + }) + it(`rejects a conditional config with a top-level scalar result`, () => { const collection = createCollection( mockSyncCollectionOptions({ diff --git a/packages/react-db/tests/useLiveQuery.test.tsx b/packages/react-db/tests/useLiveQuery.test.tsx index 0ba4587c8..0f880a949 100644 --- a/packages/react-db/tests/useLiveQuery.test.tsx +++ b/packages/react-db/tests/useLiveQuery.test.tsx @@ -2051,6 +2051,48 @@ describe(`Query Collections`, () => { expect(result.current.isEnabled).toBe(false) }) + it(`disables a config query with deprecated dependencies`, async () => { + const warnSpy = vi.spyOn(console, `warn`).mockImplementation(() => {}) + const collection = createCollection( + mockSyncCollectionOptions({ + id: `conditional-config-deps-test`, + getKey: (person: Person) => person.id, + initialData: initialPersons, + }), + ) + + const { result, rerender } = renderHook( + ({ enabled }: { enabled: boolean }) => + useLiveQuery( + { + query: (q) => { + if (!enabled) return undefined + return q + .from({ persons: collection }) + .where(({ persons }) => gt(persons.age, 30)) + }, + }, + [enabled], + ), + { initialProps: { enabled: false } }, + ) + + expect(result.current.status).toBe(`disabled`) + expect(result.current.isEnabled).toBe(false) + + rerender({ enabled: true }) + + await waitFor(() => expect(result.current.data).toHaveLength(1)) + expect(result.current.status).toBe(`ready`) + expect(result.current.isEnabled).toBe(true) + + rerender({ enabled: false }) + + expect(result.current.status).toBe(`disabled`) + expect(result.current.isEnabled).toBe(false) + warnSpy.mockRestore() + }) + it(`should handle callback returning undefined without a dependency array`, async () => { const collection = createCollection( mockSyncCollectionOptions({ From 79f0d8becf005c3769b45fb57a05665b1656be8c Mon Sep 17 00:00:00 2001 From: Kyle Mathews Date: Fri, 21 Aug 2026 16:07:16 -0600 Subject: [PATCH 4/4] test: cover disabling during live query sync --- packages/react-db/tests/useLiveQuery.test.tsx | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/packages/react-db/tests/useLiveQuery.test.tsx b/packages/react-db/tests/useLiveQuery.test.tsx index 0f880a949..87e678e1d 100644 --- a/packages/react-db/tests/useLiveQuery.test.tsx +++ b/packages/react-db/tests/useLiveQuery.test.tsx @@ -2093,6 +2093,57 @@ describe(`Query Collections`, () => { warnSpy.mockRestore() }) + it(`stays disabled when the prior query becomes ready`, async () => { + let finishSync: (() => void) | undefined + const collection = createCollection({ + id: `conditional-config-pending-sync-test`, + getKey: (person) => person.id, + sync: { + sync: ({ begin, write, commit, markReady }) => { + finishSync = () => { + begin() + write({ type: `insert`, value: initialPersons[2]! }) + commit() + markReady() + } + }, + }, + onInsert: async () => {}, + onUpdate: async () => {}, + onDelete: async () => {}, + }) + + const { result, rerender } = renderHook( + ({ enabled }: { enabled: boolean }) => + useLiveQuery({ + query: (q) => { + if (!enabled) return undefined + return q + .from({ persons: collection }) + .where(({ persons }) => gt(persons.age, 30)) + }, + }), + { initialProps: { enabled: true } }, + ) + + await waitFor(() => expect(finishSync).toBeDefined()) + expect(result.current.isLoading).toBe(true) + + rerender({ enabled: false }) + expect(result.current.status).toBe(`disabled`) + + await act(async () => { + finishSync!() + await Promise.resolve() + }) + + expect(collection.status).toBe(`ready`) + expect(collection.state.size).toBe(1) + expect(result.current.status).toBe(`disabled`) + expect(result.current.data).toBeUndefined() + expect(result.current.collection).toBeUndefined() + }) + it(`should handle callback returning undefined without a dependency array`, async () => { const collection = createCollection( mockSyncCollectionOptions({