import { useRef, useSyncExternalStore } from 'react'
import {
BaseQueryBuilder,
CollectionImpl,
createLiveQueryCollection,
} from '@tanstack/db'
import type {
Collection,
CollectionConfigSingleRowOption,
CollectionStatus,
Context,
GetResult,
InferResultType,
InitialQueryBuilder,
LiveQueryCollectionConfig,
NonSingleResult,
QueryBuilder,
SingleResult,
} from '@tanstack/db'
const DEFAULT_GC_TIME_MS = 1 // Live queries created by useLiveQuery are cleaned up immediately (0 disables GC)
export type UseLiveQueryStatus = CollectionStatus | `disabled`
/**
* Create a live query using a query function
* @param queryFn - Query function that defines what data to fetch
* @param deps - Array of dependencies that trigger query re-execution when changed
* @returns Object with reactive data, state, and status information
* @example
* // Basic query with object syntax
* const { data, isLoading } = useLiveQuery((q) =>
* q.from({ todos: todosCollection })
* .where(({ todos }) => eq(todos.completed, false))
* .select(({ todos }) => ({ id: todos.id, text: todos.text }))
* )
*
* @example
* // Single result query
* const { data } = useLiveQuery(
* (q) => q.from({ todos: todosCollection })
* .where(({ todos }) => eq(todos.id, 1))
* .findOne()
* )
*
* @example
* // With dependencies that trigger re-execution
* const { data, state } = useLiveQuery(
* (q) => q.from({ todos: todosCollection })
* .where(({ todos }) => gt(todos.priority, minPriority)),
* [minPriority] // Re-run when minPriority changes
* )
*
* @example
* // Join pattern
* const { data } = useLiveQuery((q) =>
* q.from({ issues: issueCollection })
* .join({ persons: personCollection }, ({ issues, persons }) =>
* eq(issues.userId, persons.id)
* )
* .select(({ issues, persons }) => ({
* id: issues.id,
* title: issues.title,
* userName: persons.name
* }))
* )
*
* @example
* // Handle loading and error states
* const { data, isLoading, isError, status } = useLiveQuery((q) =>
* q.from({ todos: todoCollection })
* )
*
* if (isLoading) return
Loading...
* if (isError) return Error: {status}
*
* return (
*
* {data.map(todo => - {todo.text}
)}
*
* )
*/
// Overload 1: Accept query function that always returns QueryBuilder
export function useLiveQuery(
queryFn: (q: InitialQueryBuilder) => QueryBuilder,
deps?: Array,
): {
state: Map>
data: InferResultType
collection: Collection, string | number, {}>
status: CollectionStatus // Can't be disabled if always returns QueryBuilder
isLoading: boolean
isReady: boolean
isIdle: boolean
isError: boolean
isCleanedUp: boolean
isEnabled: true // Always true if always returns QueryBuilder
}
// Overload 2: Accept query function that can return undefined/null
export function useLiveQuery(
queryFn: (
q: InitialQueryBuilder,
) => QueryBuilder | undefined | null,
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
}
// Overload 3: Accept query function that can return LiveQueryCollectionConfig
export function useLiveQuery(
queryFn: (
q: InitialQueryBuilder,
) => LiveQueryCollectionConfig | undefined | null,
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
}
// Overload 4: Accept query function that can return Collection
export function useLiveQuery<
TResult extends object,
TKey extends string | number,
TUtils extends Record,
>(
queryFn: (
q: InitialQueryBuilder,
) => Collection | undefined | null,
deps?: Array,
): {
state: Map | undefined
data: Array | undefined
collection: Collection | undefined
status: UseLiveQueryStatus
isLoading: boolean
isReady: boolean
isIdle: boolean
isError: boolean
isCleanedUp: boolean
isEnabled: boolean
}
// Overload 5: Accept query function that can return all types
export function useLiveQuery<
TContext extends Context,
TResult extends object,
TKey extends string | number,
TUtils extends Record,
>(
queryFn: (
q: InitialQueryBuilder,
) =>
| QueryBuilder
| LiveQueryCollectionConfig
| Collection
| undefined
| null,
deps?: Array,
): {
state:
| Map>
| Map
| undefined
data: InferResultType | Array | undefined
collection:
| Collection, string | number, {}>
| Collection
| undefined
status: UseLiveQueryStatus
isLoading: boolean
isReady: boolean
isIdle: boolean
isError: boolean
isCleanedUp: boolean
isEnabled: boolean
}
/**
* Create a live query using configuration object
* @param config - Configuration object with query and options
* @param deps - Array of dependencies that trigger query re-execution when changed
* @returns Object with reactive data, state, and status information
* @example
* // Basic config object usage
* const { data, status } = useLiveQuery({
* query: (q) => q.from({ todos: todosCollection }),
* gcTime: 60000
* })
*
* @example
* // With query builder and options
* const queryBuilder = new Query()
* .from({ persons: collection })
* .where(({ persons }) => gt(persons.age, 30))
* .select(({ persons }) => ({ id: persons.id, name: persons.name }))
*
* const { data, isReady } = useLiveQuery({ query: queryBuilder })
*
* @example
* // Handle all states uniformly
* const { data, isLoading, isReady, isError } = useLiveQuery({
* query: (q) => q.from({ items: itemCollection })
* })
*
* if (isLoading) return Loading...
* if (isError) return Something went wrong
* if (!isReady) return Preparing...
*
* return {data.length} items loaded
*/
// Overload 6: Accept config object
export function useLiveQuery(
config: LiveQueryCollectionConfig,
deps?: Array,
): {
state: Map>
data: InferResultType
collection: Collection, string | number, {}>
status: CollectionStatus // Can't be disabled for config objects
isLoading: boolean
isReady: boolean
isIdle: boolean
isError: boolean
isCleanedUp: boolean
isEnabled: true // Always true for config objects
}
/**
* Subscribe to an existing live query collection
* @param liveQueryCollection - Pre-created live query collection to subscribe to
* @returns Object with reactive data, state, and status information
* @example
* // Using pre-created live query collection
* const myLiveQuery = createLiveQueryCollection((q) =>
* q.from({ todos: todosCollection }).where(({ todos }) => eq(todos.active, true))
* )
* const { data, collection } = useLiveQuery(myLiveQuery)
*
* @example
* // Access collection methods directly
* const { data, collection, isReady } = useLiveQuery(existingCollection)
*
* // Use collection for mutations
* const handleToggle = (id) => {
* collection.update(id, draft => { draft.completed = !draft.completed })
* }
*
* @example
* // Handle states consistently
* const { data, isLoading, isError } = useLiveQuery(sharedCollection)
*
* if (isLoading) return Loading...
* if (isError) return Error loading data
*
* return {data.map(item => )}
*/
// Overload 7: Accept pre-created live query collection
export function useLiveQuery<
TResult extends object,
TKey extends string | number,
TUtils extends Record,
>(
liveQueryCollection: Collection & NonSingleResult,
): {
state: Map
data: Array
collection: Collection
status: CollectionStatus // Can't be disabled for pre-created live query collections
isLoading: boolean
isReady: boolean
isIdle: boolean
isError: boolean
isCleanedUp: boolean
isEnabled: true // Always true for pre-created live query collections
}
// Overload 8: Accept pre-created live query collection with singleResult: true
export function useLiveQuery<
TResult extends object,
TKey extends string | number,
TUtils extends Record,
>(
liveQueryCollection: Collection & SingleResult,
): {
state: Map
data: TResult | undefined
collection: Collection & SingleResult
status: CollectionStatus // Can't be disabled for pre-created live query collections
isLoading: boolean
isReady: boolean
isIdle: boolean
isError: boolean
isCleanedUp: boolean
isEnabled: true // Always true for pre-created live query collections
}
// Implementation - use function overloads to infer the actual collection type
export function useLiveQuery(
configOrQueryOrCollection: any,
deps: Array = [],
) {
// Check if it's already a collection by checking for specific collection methods
const isCollection =
configOrQueryOrCollection &&
typeof configOrQueryOrCollection === `object` &&
typeof configOrQueryOrCollection.subscribeChanges === `function` &&
typeof configOrQueryOrCollection.startSyncImmediate === `function` &&
typeof configOrQueryOrCollection.id === `string`
// Use refs to cache collection and track dependencies
const collectionRef = useRef | null>(
null,
)
const depsRef = useRef | null>(null)
const configRef = useRef(null)
// Use refs to track version and memoized snapshot
const versionRef = useRef(0)
const snapshotRef = useRef<{
collection: Collection