Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .changeset/fix-isready-disabled-queries.md
Original file line number Diff line number Diff line change
@@ -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.
51 changes: 45 additions & 6 deletions packages/angular-db/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -32,10 +32,10 @@ export interface InjectLiveQueryResult<
state: Signal<Map<TKey, TResult>>
/** A signal containing the results as an array */
data: Signal<Array<TResult>>
/** A signal containing the underlying collection instance */
collection: Signal<Collection<TResult, TKey, TUtils>>
/** A signal containing the underlying collection instance (null for disabled queries) */
collection: Signal<Collection<TResult, TKey, TUtils> | null>
/** A signal containing the current status of the collection */
status: Signal<CollectionStatus>
status: Signal<CollectionStatus | `disabled`>
/** A signal indicating whether the collection is currently loading */
isLoading: Signal<boolean>
/** A signal indicating whether the collection is ready */
Expand All @@ -58,9 +58,22 @@ export function injectLiveQuery<
q: InitialQueryBuilder
}) => QueryBuilder<TContext>
}): InjectLiveQueryResult<GetResult<TContext>>
export function injectLiveQuery<
TContext extends Context,
TParams extends any,
>(options: {
params: () => TParams
query: (args: {
params: TParams
q: InitialQueryBuilder
}) => QueryBuilder<TContext> | undefined | null
}): InjectLiveQueryResult<GetResult<TContext>>
export function injectLiveQuery<TContext extends Context>(
queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>
): InjectLiveQueryResult<GetResult<TContext>>
export function injectLiveQuery<TContext extends Context>(
queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext> | undefined | null
): InjectLiveQueryResult<GetResult<TContext>>
export function injectLiveQuery<TContext extends Context>(
config: LiveQueryCollectionConfig<TContext>
): InjectLiveQueryResult<GetResult<TContext>>
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -123,7 +155,9 @@ export function injectLiveQuery(opts: any) {

const state = signal(new Map<string | number, any>())
const data = signal<Array<any>>([])
const status = signal<CollectionStatus>(`idle`)
const status = signal<CollectionStatus | `disabled`>(
collection() ? `idle` : `disabled`
)

const syncDataFromCollection = (
currentCollection: Collection<any, any, any>
Expand All @@ -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
}

Expand Down Expand Up @@ -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`),
Expand Down
115 changes: 115 additions & 0 deletions packages/angular-db/tests/inject-live-query.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Person>({
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<Person>({
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([])
})
})
})
})
2 changes: 1 addition & 1 deletion packages/react-db/src/useLiveQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -492,7 +492,7 @@ export function useLiveQuery(
collection: undefined,
status: `disabled`,
isLoading: false,
isReady: false,
isReady: true,
isIdle: false,
isError: false,
isCleanedUp: false,
Expand Down
6 changes: 3 additions & 3 deletions packages/react-db/tests/useLiveQuery.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
Loading
Loading