Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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.
45 changes: 42 additions & 3 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 Down Expand Up @@ -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
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
65 changes: 59 additions & 6 deletions packages/solid-db/src/useLiveQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -76,7 +80,7 @@ import type {
* </Switch>
* )
*/
// Overload 1: Accept just the query function
// Overload 1: Accept query function that always returns QueryBuilder
export function useLiveQuery<TContext extends Context>(
queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>
): {
Expand All @@ -91,6 +95,25 @@ export function useLiveQuery<TContext extends Context>(
isCleanedUp: Accessor<boolean>
}

// Overload 1b: Accept query function that can return undefined/null
export function useLiveQuery<TContext extends Context>(
queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext> | undefined | null
): {
state: ReactiveMap<string | number, GetResult<TContext>>
data: Array<GetResult<TContext>>
collection: Accessor<Collection<
GetResult<TContext>,
string | number,
{}
> | null>
status: Accessor<CollectionStatus | `disabled`>
isLoading: Accessor<boolean>
isReady: Accessor<boolean>
isIdle: Accessor<boolean>
isError: Accessor<boolean>
isCleanedUp: Accessor<boolean>
}

/**
* Create a live query using configuration object
* @param config - Configuration object with query and options
Expand Down Expand Up @@ -207,13 +230,28 @@ 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,
})
}

const innerCollection = configOrQueryOrCollection()

if (innerCollection === undefined || innerCollection === null) {
// Disabled query - return null
return null
}

if (innerCollection instanceof CollectionImpl) {
innerCollection.startSyncImmediate()
return innerCollection as Collection
Expand All @@ -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 = (
Expand All @@ -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)

Expand Down Expand Up @@ -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`,
Expand Down
39 changes: 35 additions & 4 deletions packages/svelte-db/src/useLiveQuery.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -151,12 +151,20 @@ function toValue<T>(value: MaybeGetter<T>): T {
* // </ul>
* // {/if}
*/
// Overload 1: Accept just the query function
// Overload 1: Accept query function that always returns QueryBuilder
export function useLiveQuery<TContext extends Context>(
queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>,
deps?: Array<() => unknown>
): UseLiveQueryReturn<GetResult<TContext>>

// Overload 1b: Accept query function that can return undefined/null
export function useLiveQuery<TContext extends Context>(
queryFn: (
q: InitialQueryBuilder
) => QueryBuilder<TContext> | undefined | null,
deps?: Array<() => unknown>
): UseLiveQueryReturn<GetResult<TContext>>

/**
* Create a live query using configuration object
* @param config - Configuration object with query and options
Expand Down Expand Up @@ -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,
Expand All @@ -312,7 +329,7 @@ export function useLiveQuery(
let internalData = $state<Array<any>>([])

// 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 = (
Expand All @@ -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

Expand Down Expand Up @@ -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`
Expand Down
Loading
Loading