@tanstack/svelte-db
Version:
Svelte integration for @tanstack/db
211 lines (210 loc) • 8.14 kB
TypeScript
import type { Collection, CollectionStatus, Context, GetResult, InferResultType, InitialQueryBuilder, LiveQueryCollectionConfig, NonSingleResult, QueryBuilder, SingleResult } from '@tanstack/db';
/**
* Return type for useLiveQuery hook
* @property state - Reactive Map of query results (key → item)
* @property data - Reactive array of query results in order, or single item when using findOne()
* @property collection - The underlying query collection instance
* @property status - Current query status
* @property isLoading - True while initial query data is loading
* @property isReady - True when query has received first data and is ready
* @property isIdle - True when query hasn't started yet
* @property isError - True when query encountered an error
* @property isCleanedUp - True when query has been cleaned up
*/
export interface UseLiveQueryReturn<T extends object, TData = Array<T>> {
state: Map<string | number, T>;
data: TData;
collection: Collection<T, string | number, {}>;
status: CollectionStatus;
isLoading: boolean;
isReady: boolean;
isIdle: boolean;
isError: boolean;
isCleanedUp: boolean;
}
export interface UseLiveQueryReturnWithCollection<T extends object, TKey extends string | number, TUtils extends Record<string, any>, TData = Array<T>> {
state: Map<TKey, T>;
data: TData;
collection: Collection<T, TKey, TUtils>;
status: CollectionStatus;
isLoading: boolean;
isReady: boolean;
isIdle: boolean;
isError: boolean;
isCleanedUp: boolean;
}
type MaybeGetter<T> = T | (() => T);
/**
* Create a live query using a query function
* @param queryFn - Query function that defines what data to fetch
* @param deps - Array of reactive dependencies that trigger query re-execution when changed
* @returns Reactive object with query data, state, and status information
*
* @remarks
* **IMPORTANT - Destructuring in Svelte 5:**
* Direct destructuring breaks reactivity. To destructure, wrap with `$derived`:
*
* ❌ **Incorrect** - Loses reactivity:
* ```ts
* const { data, isLoading } = useLiveQuery(...)
* ```
*
* ✅ **Correct** - Maintains reactivity:
* ```ts
* // Option 1: Use dot notation (recommended)
* const query = useLiveQuery(...)
* // Access: query.data, query.isLoading
*
* // Option 2: Wrap with $derived for destructuring
* const query = useLiveQuery(...)
* const { data, isLoading } = $derived(query)
* ```
*
* This is a fundamental Svelte 5 limitation, not a library bug. See:
* https://github.com/sveltejs/svelte/issues/11002
*
* @example
* // Basic query with object syntax (recommended pattern)
* const todosQuery = useLiveQuery((q) =>
* q.from({ todos: todosCollection })
* .where(({ todos }) => eq(todos.completed, false))
* .select(({ todos }) => ({ id: todos.id, text: todos.text }))
* )
* // Access via: todosQuery.data, todosQuery.isLoading, etc.
*
* @example
* // With reactive dependencies
* let minPriority = $state(5)
* const todosQuery = useLiveQuery(
* (q) => q.from({ todos: todosCollection })
* .where(({ todos }) => gt(todos.priority, minPriority)),
* [() => minPriority] // Re-run when minPriority changes
* )
*
* @example
* // Destructuring with $derived (if needed)
* const query = useLiveQuery((q) =>
* q.from({ todos: todosCollection })
* )
* const { data, isLoading, isError } = $derived(query)
* // Now data, isLoading, and isError maintain reactivity
*
* @example
* // Join pattern
* const issuesQuery = 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 in template
* const todosQuery = useLiveQuery((q) =>
* q.from({ todos: todoCollection })
* )
*
* // In template:
* // {#if todosQuery.isLoading}
* // <div>Loading...</div>
* // {:else if todosQuery.isError}
* // <div>Error: {todosQuery.status}</div>
* // {:else}
* // <ul>
* // {#each todosQuery.data as todo (todo.id)}
* // <li>{todo.text}</li>
* // {/each}
* // </ul>
* // {/if}
*/
export declare function useLiveQuery<TContext extends Context>(queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>, deps?: Array<() => unknown>): UseLiveQueryReturn<GetResult<TContext>, InferResultType<TContext>>;
export declare function useLiveQuery<TContext extends Context>(queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext> | undefined | null, deps?: Array<() => unknown>): UseLiveQueryReturn<GetResult<TContext>, InferResultType<TContext> | undefined>;
/**
* Create a live query using configuration object
* @param config - Configuration object with query and options
* @param deps - Array of reactive dependencies that trigger query re-execution when changed
* @returns Reactive object with query data, state, and status information
* @example
* // Basic config object usage
* const todosQuery = useLiveQuery({
* query: (q) => q.from({ todos: todosCollection }),
* gcTime: 60000
* })
*
* @example
* // With reactive dependencies
* let filter = $state('active')
* const todosQuery = useLiveQuery({
* query: (q) => q.from({ todos: todosCollection })
* .where(({ todos }) => eq(todos.status, filter))
* }, [() => filter])
*
* @example
* // Handle all states uniformly
* const itemsQuery = useLiveQuery({
* query: (q) => q.from({ items: itemCollection })
* })
*
* // In template:
* // {#if itemsQuery.isLoading}
* // <div>Loading...</div>
* // {:else if itemsQuery.isError}
* // <div>Something went wrong</div>
* // {:else if !itemsQuery.isReady}
* // <div>Preparing...</div>
* // {:else}
* // <div>{itemsQuery.data.length} items loaded</div>
* // {/if}
*/
export declare function useLiveQuery<TContext extends Context>(config: LiveQueryCollectionConfig<TContext>, deps?: Array<() => unknown>): UseLiveQueryReturn<GetResult<TContext>, InferResultType<TContext>>;
/**
* Subscribe to an existing query collection (can be reactive)
* @param liveQueryCollection - Pre-created query collection to subscribe to (can be a getter)
* @returns Reactive object with query data, state, and status information
* @example
* // Using pre-created query collection
* const myLiveQuery = createLiveQueryCollection((q) =>
* q.from({ todos: todosCollection }).where(({ todos }) => eq(todos.active, true))
* )
* const queryResult = useLiveQuery(myLiveQuery)
*
* @example
* // Reactive query collection reference
* let selectedQuery = $state(todosQuery)
* const queryResult = useLiveQuery(() => selectedQuery)
*
* // Switch queries reactively
* selectedQuery = archiveQuery
*
* @example
* // Access query collection methods directly
* const queryResult = useLiveQuery(existingQuery)
*
* // Use underlying collection for mutations
* const handleToggle = (id) => {
* queryResult.collection.update(id, draft => { draft.completed = !draft.completed })
* }
*
* @example
* // Handle states consistently
* const queryResult = useLiveQuery(sharedQuery)
*
* // In template:
* // {#if queryResult.isLoading}
* // <div>Loading...</div>
* // {:else if queryResult.isError}
* // <div>Error loading data</div>
* // {:else}
* // {#each queryResult.data as item (item.id)}
* // <Item {...item} />
* // {/each}
* // {/if}
*/
export declare function useLiveQuery<TResult extends object, TKey extends string | number, TUtils extends Record<string, any>>(liveQueryCollection: MaybeGetter<Collection<TResult, TKey, TUtils> & NonSingleResult>): UseLiveQueryReturnWithCollection<TResult, TKey, TUtils, Array<TResult>>;
export declare function useLiveQuery<TResult extends object, TKey extends string | number, TUtils extends Record<string, any>>(liveQueryCollection: MaybeGetter<Collection<TResult, TKey, TUtils> & SingleResult>): UseLiveQueryReturnWithCollection<TResult, TKey, TUtils, TResult | undefined>;
export {};