diff --git a/.changeset/fix-isready-disabled-queries.md b/.changeset/fix-isready-disabled-queries.md new file mode 100644 index 000000000..132902238 --- /dev/null +++ b/.changeset/fix-isready-disabled-queries.md @@ -0,0 +1,13 @@ +--- +"@tanstack/react-db": patch +"@tanstack/solid-db": patch +"@tanstack/vue-db": patch +"@tanstack/svelte-db": patch +"@tanstack/angular-db": patch +--- + +Fixed `isReady` to return `true` for disabled queries in `useLiveQuery`/`injectLiveQuery` across all framework packages. When a query function returns `null` or `undefined` (disabling the query), there's no async operation to wait for, so the hook should be considered "ready" immediately. + +Additionally, all frameworks now have proper TypeScript overloads that explicitly support returning `undefined | null` from query functions, making the disabled query pattern type-safe. + +This fixes the common pattern where users conditionally enable queries and don't want to show loading states when the query is disabled. diff --git a/packages/angular-db/src/index.ts b/packages/angular-db/src/index.ts index b2080b3e4..2220dc85f 100644 --- a/packages/angular-db/src/index.ts +++ b/packages/angular-db/src/index.ts @@ -6,7 +6,7 @@ import { inject, signal, } from "@angular/core" -import { createLiveQueryCollection } from "@tanstack/db" +import { BaseQueryBuilder, createLiveQueryCollection } from "@tanstack/db" import type { ChangeMessage, Collection, @@ -32,10 +32,10 @@ export interface InjectLiveQueryResult< state: Signal> /** A signal containing the results as an array */ data: Signal> - /** A signal containing the underlying collection instance */ - collection: Signal> + /** A signal containing the underlying collection instance (null for disabled queries) */ + collection: Signal | null> /** A signal containing the current status of the collection */ - status: Signal + status: Signal /** A signal indicating whether the collection is currently loading */ isLoading: Signal /** A signal indicating whether the collection is ready */ @@ -58,9 +58,22 @@ export function injectLiveQuery< q: InitialQueryBuilder }) => QueryBuilder }): InjectLiveQueryResult> +export function injectLiveQuery< + TContext extends Context, + TParams extends any, +>(options: { + params: () => TParams + query: (args: { + params: TParams + q: InitialQueryBuilder + }) => QueryBuilder | undefined | null +}): InjectLiveQueryResult> export function injectLiveQuery( queryFn: (q: InitialQueryBuilder) => QueryBuilder ): InjectLiveQueryResult> +export function injectLiveQuery( + queryFn: (q: InitialQueryBuilder) => QueryBuilder | undefined | null +): InjectLiveQueryResult> export function injectLiveQuery( config: LiveQueryCollectionConfig ): InjectLiveQueryResult> @@ -89,6 +102,15 @@ export function injectLiveQuery(opts: any) { } if (typeof opts === `function`) { + // Check if query function returns null/undefined (disabled query) + const queryBuilder = new BaseQueryBuilder() as InitialQueryBuilder + const result = opts(queryBuilder) + + if (result === undefined || result === null) { + // Disabled query - return null + return null + } + return createLiveQueryCollection({ query: opts, startSync: true, @@ -106,6 +128,16 @@ export function injectLiveQuery(opts: any) { if (isReactiveQueryOptions) { const { params, query } = opts const currentParams = params() + + // Check if query function returns null/undefined (disabled query) + const queryBuilder = new BaseQueryBuilder() as InitialQueryBuilder + const result = query({ params: currentParams, q: queryBuilder }) + + if (result === undefined || result === null) { + // Disabled query - return null + return null + } + return createLiveQueryCollection({ query: (q) => query({ params: currentParams, q }), startSync: true, @@ -123,7 +155,9 @@ export function injectLiveQuery(opts: any) { const state = signal(new Map()) const data = signal>([]) - const status = signal(`idle`) + const status = signal( + collection() ? `idle` : `disabled` + ) const syncDataFromCollection = ( currentCollection: Collection @@ -145,7 +179,12 @@ export function injectLiveQuery(opts: any) { effect((onCleanup) => { const currentCollection = collection() + // Handle null collection (disabled query) if (!currentCollection) { + status.set(`disabled` as const) + state.set(new Map()) + data.set([]) + cleanup() return } @@ -185,7 +224,7 @@ export function injectLiveQuery(opts: any) { collection, status, isLoading: computed(() => status() === `loading`), - isReady: computed(() => status() === `ready`), + isReady: computed(() => status() === `ready` || status() === `disabled`), isIdle: computed(() => status() === `idle`), isError: computed(() => status() === `error`), isCleanedUp: computed(() => status() === `cleaned-up`), diff --git a/packages/angular-db/tests/inject-live-query.test.ts b/packages/angular-db/tests/inject-live-query.test.ts index 6579bd0a3..7c721b453 100644 --- a/packages/angular-db/tests/inject-live-query.test.ts +++ b/packages/angular-db/tests/inject-live-query.test.ts @@ -1031,4 +1031,119 @@ describe(`injectLiveQuery`, () => { }) }) }) + + describe(`disabled queries`, () => { + it(`should handle query function returning undefined with proper state`, async () => { + await TestBed.runInInjectionContext(async () => { + const collection = createCollection( + mockSyncCollectionOptions({ + id: `disabled-test-undefined-angular`, + getKey: (person: Person) => person.id, + initialData: initialPersons, + }) + ) + + const enabled = signal(false) + + const result = injectLiveQuery({ + params: () => ({ enabled: enabled() }), + query: ({ params, q }) => { + if (!params.enabled) return undefined + return q + .from({ persons: collection }) + .where(({ persons }) => eq(persons.isActive, true)) + .select(({ persons }) => ({ + id: persons.id, + name: persons.name, + })) + }, + }) + + await waitForAngularUpdate() + + // When disabled, status should be 'disabled' and isReady should be true + expect(result.status()).toBe(`disabled`) + expect(result.isReady()).toBe(true) + expect(result.isLoading()).toBe(false) + expect(result.isIdle()).toBe(false) + expect(result.isError()).toBe(false) + expect(result.collection()).toBeNull() + expect(result.data()).toEqual([]) + expect(result.state().size).toBe(0) + + // Enable the query + enabled.set(true) + await waitForAngularUpdate() + + // Should now be ready with data + expect(result.status()).toBe(`ready`) + expect(result.isReady()).toBe(true) + expect(result.isLoading()).toBe(false) + expect(result.collection()).not.toBeNull() + expect(result.data().length).toBeGreaterThan(0) + }) + }) + + it(`should handle query function returning null with proper state`, async () => { + await TestBed.runInInjectionContext(async () => { + const collection = createCollection( + mockSyncCollectionOptions({ + id: `disabled-test-null-angular`, + getKey: (person: Person) => person.id, + initialData: initialPersons, + }) + ) + + const enabled = signal(false) + + const result = injectLiveQuery({ + params: () => ({ enabled: enabled() }), + query: ({ params, q }) => { + if (!params.enabled) return null + return q + .from({ persons: collection }) + .where(({ persons }) => gt(persons.age, 25)) + .select(({ persons }) => ({ + id: persons.id, + name: persons.name, + age: persons.age, + })) + }, + }) + + await waitForAngularUpdate() + + // When disabled, status should be 'disabled' and isReady should be true + expect(result.status()).toBe(`disabled`) + expect(result.isReady()).toBe(true) + expect(result.isLoading()).toBe(false) + expect(result.isIdle()).toBe(false) + expect(result.isError()).toBe(false) + expect(result.collection()).toBeNull() + expect(result.data()).toEqual([]) + expect(result.state().size).toBe(0) + + // Enable the query + enabled.set(true) + await waitForAngularUpdate() + + // Should now be ready with data + expect(result.status()).toBe(`ready`) + expect(result.isReady()).toBe(true) + expect(result.isLoading()).toBe(false) + expect(result.collection()).not.toBeNull() + expect(result.data().length).toBeGreaterThan(0) + + // Disable again + enabled.set(false) + await waitForAngularUpdate() + + // Should go back to disabled state + expect(result.status()).toBe(`disabled`) + expect(result.isReady()).toBe(true) + expect(result.collection()).toBeNull() + expect(result.data()).toEqual([]) + }) + }) + }) }) diff --git a/packages/react-db/src/useLiveQuery.ts b/packages/react-db/src/useLiveQuery.ts index 90145d25d..8fd0d2eb0 100644 --- a/packages/react-db/src/useLiveQuery.ts +++ b/packages/react-db/src/useLiveQuery.ts @@ -492,7 +492,7 @@ export function useLiveQuery( collection: undefined, status: `disabled`, isLoading: false, - isReady: false, + isReady: true, isIdle: false, isError: false, isCleanedUp: false, diff --git a/packages/react-db/tests/useLiveQuery.test.tsx b/packages/react-db/tests/useLiveQuery.test.tsx index fec064163..fb7ab5dbc 100644 --- a/packages/react-db/tests/useLiveQuery.test.tsx +++ b/packages/react-db/tests/useLiveQuery.test.tsx @@ -2005,7 +2005,7 @@ describe(`Query Collections`, () => { expect(result.current.collection).toBeUndefined() expect(result.current.status).toBe(`disabled`) expect(result.current.isLoading).toBe(false) - expect(result.current.isReady).toBe(false) + expect(result.current.isReady).toBe(true) expect(result.current.isIdle).toBe(false) expect(result.current.isError).toBe(false) expect(result.current.isCleanedUp).toBe(false) @@ -2044,7 +2044,7 @@ describe(`Query Collections`, () => { expect(result.current.collection).toBeUndefined() expect(result.current.status).toBe(`disabled`) expect(result.current.isLoading).toBe(false) - expect(result.current.isReady).toBe(false) + expect(result.current.isReady).toBe(true) expect(result.current.isIdle).toBe(false) expect(result.current.isError).toBe(false) expect(result.current.isCleanedUp).toBe(false) @@ -2085,7 +2085,7 @@ describe(`Query Collections`, () => { expect(result.current.collection).toBeUndefined() expect(result.current.status).toBe(`disabled`) expect(result.current.isLoading).toBe(false) - expect(result.current.isReady).toBe(false) + expect(result.current.isReady).toBe(true) expect(result.current.isIdle).toBe(false) expect(result.current.isError).toBe(false) expect(result.current.isCleanedUp).toBe(false) diff --git a/packages/solid-db/src/useLiveQuery.ts b/packages/solid-db/src/useLiveQuery.ts index d3e645871..9654ac20b 100644 --- a/packages/solid-db/src/useLiveQuery.ts +++ b/packages/solid-db/src/useLiveQuery.ts @@ -7,7 +7,11 @@ import { onCleanup, } from "solid-js" import { ReactiveMap } from "@solid-primitives/map" -import { CollectionImpl, createLiveQueryCollection } from "@tanstack/db" +import { + BaseQueryBuilder, + CollectionImpl, + createLiveQueryCollection, +} from "@tanstack/db" import { createStore, reconcile } from "solid-js/store" import type { Accessor } from "solid-js" import type { @@ -76,7 +80,7 @@ import type { * * ) */ -// Overload 1: Accept just the query function +// Overload 1: Accept query function that always returns QueryBuilder export function useLiveQuery( queryFn: (q: InitialQueryBuilder) => QueryBuilder ): { @@ -91,6 +95,25 @@ export function useLiveQuery( isCleanedUp: Accessor } +// Overload 1b: Accept query function that can return undefined/null +export function useLiveQuery( + queryFn: (q: InitialQueryBuilder) => QueryBuilder | undefined | null +): { + state: ReactiveMap> + data: Array> + collection: Accessor, + string | number, + {} + > | null> + status: Accessor + isLoading: Accessor + isReady: Accessor + isIdle: Accessor + isError: Accessor + isCleanedUp: Accessor +} + /** * Create a live query using configuration object * @param config - Configuration object with query and options @@ -207,6 +230,15 @@ export function useLiveQuery( const collection = createMemo( () => { if (configOrQueryOrCollection.length === 1) { + // This is a query function - check if it returns null/undefined + const queryBuilder = new BaseQueryBuilder() as InitialQueryBuilder + const result = configOrQueryOrCollection(queryBuilder) + + if (result === undefined || result === null) { + // Disabled query - return null + return null + } + return createLiveQueryCollection({ query: configOrQueryOrCollection, startSync: true, @@ -214,6 +246,12 @@ export function useLiveQuery( } const innerCollection = configOrQueryOrCollection() + + if (innerCollection === undefined || innerCollection === null) { + // Disabled query - return null + return null + } + if (innerCollection instanceof CollectionImpl) { innerCollection.startSyncImmediate() return innerCollection as Collection @@ -237,9 +275,12 @@ export function useLiveQuery( }) // Track collection status reactively - const [status, setStatus] = createSignal(collection().status, { - name: `TanstackDBStatus`, - }) + const [status, setStatus] = createSignal( + collection() ? collection()!.status : (`disabled` as const), + { + name: `TanstackDBStatus`, + } + ) // Helper to sync data array from collection in correct order const syncDataFromCollection = ( @@ -257,6 +298,18 @@ export function useLiveQuery( () => { const currentCollection = collection() + // Handle null collection (disabled query) + if (!currentCollection) { + setStatus(`disabled` as const) + state.clear() + setData([]) + if (currentUnsubscribe) { + currentUnsubscribe() + currentUnsubscribe = null + } + return + } + // Update status ref whenever the effect runs setStatus(currentCollection.status) @@ -320,7 +373,7 @@ export function useLiveQuery( collection, status, isLoading: () => status() === `loading`, - isReady: () => status() === `ready`, + isReady: () => status() === `ready` || status() === `disabled`, isIdle: () => status() === `idle`, isError: () => status() === `error`, isCleanedUp: () => status() === `cleaned-up`, diff --git a/packages/solid-db/tests/useLiveQuery.test.tsx b/packages/solid-db/tests/useLiveQuery.test.tsx index 459a10bd1..db59f828a 100644 --- a/packages/solid-db/tests/useLiveQuery.test.tsx +++ b/packages/solid-db/tests/useLiveQuery.test.tsx @@ -1748,4 +1748,113 @@ describe(`Query Collections`, () => { expect(result.status()).toBe(`ready`) }) }) + + describe(`Disabled queries`, () => { + it(`should handle callback returning undefined with proper state`, async () => { + return createRoot(async (dispose) => { + const collection = createCollection( + mockSyncCollectionOptions({ + id: `disabled-undefined-test`, + getKey: (person: Person) => person.id, + initialData: initialPersons, + }) + ) + + const [enabled, setEnabled] = createSignal(false) + const rendered = renderHook( + (props: { enabled: Accessor }) => { + return useLiveQuery((q) => { + if (!props.enabled()) return undefined + return q + .from({ collection }) + .where(({ collection: c }) => gt(c.age, 30)) + .select(({ collection: c }) => ({ + id: c.id, + name: c.name, + age: c.age, + })) + }) + }, + { initialProps: [{ enabled }] } + ) + + // When callback returns undefined, should return disabled state + expect(rendered.result.state.size).toBe(0) + expect(rendered.result.data).toEqual([]) + expect(rendered.result.collection()).toBeNull() + expect(rendered.result.status()).toBe(`disabled`) + expect(rendered.result.isLoading()).toBe(false) + expect(rendered.result.isReady()).toBe(true) + + // Enable the query + setEnabled(true) + await new Promise((resolve) => setTimeout(resolve, 10)) + + await waitFor(() => { + expect(rendered.result.state.size).toBe(1) // Only John Smith (age 35) + }) + expect(rendered.result.data).toHaveLength(1) + expect(rendered.result.isReady()).toBe(true) + + // Disable the query again + setEnabled(false) + await new Promise((resolve) => setTimeout(resolve, 10)) + + expect(rendered.result.status()).toBe(`disabled`) + expect(rendered.result.isReady()).toBe(true) + + dispose() + }) + }) + + it(`should handle callback returning null with proper state`, async () => { + return createRoot(async (dispose) => { + const collection = createCollection( + mockSyncCollectionOptions({ + id: `disabled-null-test`, + getKey: (person: Person) => person.id, + initialData: initialPersons, + }) + ) + + const [enabled, setEnabled] = createSignal(false) + const rendered = renderHook( + (props: { enabled: Accessor }) => { + return useLiveQuery((q) => { + if (!props.enabled()) return null + return q + .from({ collection }) + .where(({ collection: c }) => gt(c.age, 30)) + .select(({ collection: c }) => ({ + id: c.id, + name: c.name, + age: c.age, + })) + }) + }, + { initialProps: [{ enabled }] } + ) + + // When callback returns null, should return disabled state + expect(rendered.result.state.size).toBe(0) + expect(rendered.result.data).toEqual([]) + expect(rendered.result.collection()).toBeNull() + expect(rendered.result.status()).toBe(`disabled`) + expect(rendered.result.isLoading()).toBe(false) + expect(rendered.result.isReady()).toBe(true) + + // Enable the query + setEnabled(true) + await new Promise((resolve) => setTimeout(resolve, 10)) + + await waitFor(() => { + expect(rendered.result.state.size).toBe(1) + }) + expect(rendered.result.data).toHaveLength(1) + expect(rendered.result.isReady()).toBe(true) + + dispose() + }) + }) + }) }) diff --git a/packages/svelte-db/src/useLiveQuery.svelte.ts b/packages/svelte-db/src/useLiveQuery.svelte.ts index d769d9237..af7f5b550 100644 --- a/packages/svelte-db/src/useLiveQuery.svelte.ts +++ b/packages/svelte-db/src/useLiveQuery.svelte.ts @@ -2,7 +2,7 @@ import { untrack } from "svelte" // eslint-disable-next-line import/no-duplicates -- See https://github.com/un-ts/eslint-plugin-import-x/issues/308 import { SvelteMap } from "svelte/reactivity" -import { createLiveQueryCollection } from "@tanstack/db" +import { BaseQueryBuilder, createLiveQueryCollection } from "@tanstack/db" import type { ChangeMessage, Collection, @@ -151,12 +151,20 @@ function toValue(value: MaybeGetter): T { * // * // {/if} */ -// Overload 1: Accept just the query function +// Overload 1: Accept query function that always returns QueryBuilder export function useLiveQuery( queryFn: (q: InitialQueryBuilder) => QueryBuilder, deps?: Array<() => unknown> ): UseLiveQueryReturn> +// Overload 1b: Accept query function that can return undefined/null +export function useLiveQuery( + queryFn: ( + q: InitialQueryBuilder + ) => QueryBuilder | undefined | null, + deps?: Array<() => unknown> +): UseLiveQueryReturn> + /** * Create a live query using configuration object * @param config - Configuration object with query and options @@ -293,6 +301,15 @@ export function useLiveQuery( // Ensure we always start sync for Svelte helpers if (typeof unwrappedParam === `function`) { + // Check if query function returns null/undefined (disabled query) + const queryBuilder = new BaseQueryBuilder() as InitialQueryBuilder + const result = unwrappedParam(queryBuilder) + + if (result === undefined || result === null) { + // Disabled query - return null + return null + } + return createLiveQueryCollection({ query: unwrappedParam, startSync: true, @@ -312,7 +329,7 @@ export function useLiveQuery( let internalData = $state>([]) // Track collection status reactively - let status = $state(collection.status) + let status = $state(collection ? collection.status : (`disabled` as const)) // Helper to sync data array from collection in correct order const syncDataFromCollection = ( @@ -331,6 +348,20 @@ export function useLiveQuery( $effect(() => { const currentCollection = collection + // Handle null collection (disabled query) + if (!currentCollection) { + status = `disabled` as const + untrack(() => { + state.clear() + internalData = [] + }) + if (currentUnsubscribe) { + currentUnsubscribe() + currentUnsubscribe = null + } + return + } + // Update status state whenever the effect runs status = currentCollection.status @@ -419,7 +450,7 @@ export function useLiveQuery( return status === `loading` }, get isReady() { - return status === `ready` + return status === `ready` || status === `disabled` }, get isIdle() { return status === `idle` diff --git a/packages/vue-db/src/useLiveQuery.ts b/packages/vue-db/src/useLiveQuery.ts index 298eaed91..a7116b66d 100644 --- a/packages/vue-db/src/useLiveQuery.ts +++ b/packages/vue-db/src/useLiveQuery.ts @@ -110,12 +110,20 @@ export interface UseLiveQueryReturnWithCollection< * //
  • {{ todo.text }}
  • * // */ -// Overload 1: Accept just the query function +// Overload 1: Accept query function that always returns QueryBuilder export function useLiveQuery( queryFn: (q: InitialQueryBuilder) => QueryBuilder, deps?: Array> ): UseLiveQueryReturn> +// Overload 1b: Accept query function that can return undefined/null +export function useLiveQuery( + queryFn: ( + q: InitialQueryBuilder + ) => QueryBuilder | undefined | null, + deps?: Array> +): UseLiveQueryReturn> + /** * Create a live query using configuration object * @param config - Configuration object with query and options @@ -210,15 +218,18 @@ export function useLiveQuery( const collection = computed(() => { // First check if the original parameter might be a ref/getter // by seeing if toValue returns something different than the original + // NOTE: Don't call toValue on functions - toValue treats functions as getters and calls them! let unwrappedParam = configOrQueryOrCollection - try { - const potentiallyUnwrapped = toValue(configOrQueryOrCollection) - if (potentiallyUnwrapped !== configOrQueryOrCollection) { - unwrappedParam = potentiallyUnwrapped + if (typeof configOrQueryOrCollection !== `function`) { + try { + const potentiallyUnwrapped = toValue(configOrQueryOrCollection) + if (potentiallyUnwrapped !== configOrQueryOrCollection) { + unwrappedParam = potentiallyUnwrapped + } + } catch { + // If toValue fails, use original parameter + unwrappedParam = configOrQueryOrCollection } - } catch { - // If toValue fails, use original parameter - unwrappedParam = configOrQueryOrCollection } // Check if it's already a collection by checking for specific collection methods @@ -243,10 +254,31 @@ export function useLiveQuery( // Ensure we always start sync for Vue hooks if (typeof unwrappedParam === `function`) { - return createLiveQueryCollection({ - query: unwrappedParam, - startSync: true, - }) + // To avoid calling the query function twice, we wrap it to handle null/undefined returns + // The wrapper will be called once by createLiveQueryCollection + const wrappedQuery = (q: InitialQueryBuilder) => { + const result = unwrappedParam(q) + // If the query function returns null/undefined, throw a special error + // that we'll catch to return null collection + if (result === undefined || result === null) { + throw new Error(`__DISABLED_QUERY__`) + } + return result + } + + try { + return createLiveQueryCollection({ + query: wrappedQuery, + startSync: true, + }) + } catch (error) { + // Check if this is our special disabled query marker + if (error instanceof Error && error.message === `__DISABLED_QUERY__`) { + return null + } + // Re-throw other errors + throw error + } } else { return createLiveQueryCollection({ ...unwrappedParam, @@ -265,7 +297,9 @@ export function useLiveQuery( const data = computed(() => internalData) // Track collection status reactively - const status = ref(collection.value.status) + const status = ref( + collection.value ? collection.value.status : (`disabled` as const) + ) // Helper to sync data array from collection in correct order const syncDataFromCollection = ( @@ -282,6 +316,18 @@ export function useLiveQuery( watchEffect((onInvalidate) => { const currentCollection = collection.value + // Handle null collection (disabled query) + if (!currentCollection) { + status.value = `disabled` as const + state.clear() + internalData.length = 0 + if (currentUnsubscribe) { + currentUnsubscribe() + currentUnsubscribe = null + } + return + } + // Update status ref whenever the effect runs status.value = currentCollection.status @@ -366,7 +412,9 @@ export function useLiveQuery( collection: computed(() => collection.value), status: computed(() => status.value), isLoading: computed(() => status.value === `loading`), - isReady: computed(() => status.value === `ready`), + isReady: computed( + () => status.value === `ready` || status.value === `disabled` + ), isIdle: computed(() => status.value === `idle`), isError: computed(() => status.value === `error`), isCleanedUp: computed(() => status.value === `cleaned-up`), diff --git a/packages/vue-db/tests/useLiveQuery.test.ts b/packages/vue-db/tests/useLiveQuery.test.ts index 372bba52d..60dc77cd9 100644 --- a/packages/vue-db/tests/useLiveQuery.test.ts +++ b/packages/vue-db/tests/useLiveQuery.test.ts @@ -1812,4 +1812,105 @@ describe(`Query Collections`, () => { expect(status.value).toBe(`ready`) }) }) + + describe(`Disabled queries`, () => { + it(`should handle callback returning undefined with proper state`, async () => { + const collection = createCollection( + mockSyncCollectionOptions({ + id: `disabled-undefined-test`, + getKey: (person: Person) => person.id, + initialData: initialPersons, + }) + ) + + const enabled = ref(false) + const result = useLiveQuery( + (q) => { + if (!enabled.value) return undefined + return q + .from({ persons: collection }) + .where(({ persons }) => gt(persons.age, 30)) + .select(({ persons }) => ({ + id: persons.id, + name: persons.name, + age: persons.age, + })) + }, + [() => enabled.value] + ) + + // When callback returns undefined, should return disabled state + expect(result.state.value.size).toBe(0) + expect(result.data.value).toEqual([]) + expect(result.collection.value).toBeNull() + expect(result.status.value).toBe(`disabled`) + expect(result.isLoading.value).toBe(false) + expect(result.isReady.value).toBe(true) + + // Enable the query + enabled.value = true + await waitFor(() => { + expect(result.collection.value).not.toBeNull() + }) + + await waitFor(() => { + expect(result.state.value.size).toBe(1) // Only John Smith (age 35) + }) + expect(result.data.value).toHaveLength(1) + expect(result.isReady.value).toBe(true) + + // Disable the query again + enabled.value = false + await waitFor(() => { + expect(result.status.value).toBe(`disabled`) + }) + expect(result.isReady.value).toBe(true) + }) + + it(`should handle callback returning null with proper state`, async () => { + const collection = createCollection( + mockSyncCollectionOptions({ + id: `disabled-null-test`, + getKey: (person: Person) => person.id, + initialData: initialPersons, + }) + ) + + const enabled = ref(false) + const result = useLiveQuery( + (q) => { + if (!enabled.value) return null + return q + .from({ persons: collection }) + .where(({ persons }) => gt(persons.age, 30)) + .select(({ persons }) => ({ + id: persons.id, + name: persons.name, + age: persons.age, + })) + }, + [() => enabled.value] + ) + + // When callback returns null, should return disabled state + expect(result.state.value.size).toBe(0) + expect(result.data.value).toEqual([]) + expect(result.collection.value).toBeNull() + expect(result.status.value).toBe(`disabled`) + expect(result.isLoading.value).toBe(false) + expect(result.isReady.value).toBe(true) + + // Enable the query + enabled.value = true + await waitFor(() => { + expect(result.collection.value).not.toBeNull() + }) + + await waitFor(() => { + expect(result.state.value.size).toBe(1) + }) + expect(result.data.value).toHaveLength(1) + expect(result.isReady.value).toBe(true) + }) + }) })