UNPKG

@tanstack/react-db

Version:

React integration for @tanstack/db

1 lines 7.17 kB
{"version":3,"file":"useLiveSuspenseQuery.cjs","sources":["../../src/useLiveSuspenseQuery.ts"],"sourcesContent":["import { useRef } from 'react'\nimport { useLiveQuery } from './useLiveQuery'\nimport type {\n Collection,\n Context,\n GetResult,\n InferResultType,\n InitialQueryBuilder,\n LiveQueryCollectionConfig,\n NonSingleResult,\n QueryBuilder,\n SingleResult,\n} from '@tanstack/db'\n\n/**\n * Create a live query with React Suspense support\n * @param queryFn - Query function that defines what data to fetch\n * @param deps - Array of dependencies that trigger query re-execution when changed\n * @returns Object with reactive data and state - data is guaranteed to be defined\n * @throws Promise when data is loading (caught by Suspense boundary)\n * @throws Error when collection fails (caught by Error boundary)\n * @example\n * // Basic usage with Suspense\n * function TodoList() {\n * const { data } = useLiveSuspenseQuery((q) =>\n * q.from({ todos: todosCollection })\n * .where(({ todos }) => eq(todos.completed, false))\n * .select(({ todos }) => ({ id: todos.id, text: todos.text }))\n * )\n *\n * return (\n * <ul>\n * {data.map(todo => <li key={todo.id}>{todo.text}</li>)}\n * </ul>\n * )\n * }\n *\n * function App() {\n * return (\n * <Suspense fallback={<div>Loading...</div>}>\n * <TodoList />\n * </Suspense>\n * )\n * }\n *\n * @example\n * // Single result query\n * const { data } = useLiveSuspenseQuery(\n * (q) => q.from({ todos: todosCollection })\n * .where(({ todos }) => eq(todos.id, 1))\n * .findOne()\n * )\n * // data is guaranteed to be the single item (or undefined if not found)\n *\n * @example\n * // With dependencies that trigger re-suspension\n * const { data } = useLiveSuspenseQuery(\n * (q) => q.from({ todos: todosCollection })\n * .where(({ todos }) => gt(todos.priority, minPriority)),\n * [minPriority] // Re-suspends when minPriority changes\n * )\n *\n * @example\n * // With Error boundary\n * function App() {\n * return (\n * <ErrorBoundary fallback={<div>Error loading data</div>}>\n * <Suspense fallback={<div>Loading...</div>}>\n * <TodoList />\n * </Suspense>\n * </ErrorBoundary>\n * )\n * }\n */\n// Overload 1: Accept query function that always returns QueryBuilder\nexport function useLiveSuspenseQuery<TContext extends Context>(\n queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>,\n deps?: Array<unknown>,\n): {\n state: Map<string | number, GetResult<TContext>>\n data: InferResultType<TContext>\n collection: Collection<GetResult<TContext>, string | number, {}>\n}\n\n// Overload 2: Accept config object\nexport function useLiveSuspenseQuery<TContext extends Context>(\n config: LiveQueryCollectionConfig<TContext>,\n deps?: Array<unknown>,\n): {\n state: Map<string | number, GetResult<TContext>>\n data: InferResultType<TContext>\n collection: Collection<GetResult<TContext>, string | number, {}>\n}\n\n// Overload 3: Accept pre-created live query collection\nexport function useLiveSuspenseQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n liveQueryCollection: Collection<TResult, TKey, TUtils> & NonSingleResult,\n): {\n state: Map<TKey, TResult>\n data: Array<TResult>\n collection: Collection<TResult, TKey, TUtils>\n}\n\n// Overload 4: Accept pre-created live query collection with singleResult: true\nexport function useLiveSuspenseQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n liveQueryCollection: Collection<TResult, TKey, TUtils> & SingleResult,\n): {\n state: Map<TKey, TResult>\n data: TResult | undefined\n collection: Collection<TResult, TKey, TUtils> & SingleResult\n}\n\n// Implementation - uses useLiveQuery internally and adds Suspense logic\nexport function useLiveSuspenseQuery(\n configOrQueryOrCollection: any,\n deps: Array<unknown> = [],\n) {\n const promiseRef = useRef<Promise<void> | null>(null)\n const collectionRef = useRef<Collection<any, any, any> | null>(null)\n const hasBeenReadyRef = useRef(false)\n\n // Use useLiveQuery to handle collection management and reactivity\n const result = useLiveQuery(configOrQueryOrCollection, deps)\n\n // Reset promise and ready state when collection changes (deps changed)\n if (collectionRef.current !== result.collection) {\n promiseRef.current = null\n collectionRef.current = result.collection\n hasBeenReadyRef.current = false\n }\n\n // Track when we reach ready state\n if (result.status === `ready`) {\n hasBeenReadyRef.current = true\n promiseRef.current = null\n }\n\n // SUSPENSE LOGIC: Throw promise or error based on collection status\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (!result.isEnabled) {\n // Suspense queries cannot be disabled - throw error\n throw new Error(\n `useLiveSuspenseQuery does not support disabled queries. Use useLiveQuery instead for conditional queries.`,\n )\n }\n\n // Only throw errors during initial load (before first ready)\n // After success, errors surface as stale data (matches TanStack Query behavior)\n if (result.status === `error` && !hasBeenReadyRef.current) {\n promiseRef.current = null\n // TODO: Once collections hold a reference to their last error object (#671),\n // we should rethrow that actual error instead of creating a generic message\n throw new Error(`Collection \"${result.collection.id}\" failed to load`)\n }\n\n if (result.status === `loading` || result.status === `idle`) {\n // Create or reuse promise for current collection\n if (!promiseRef.current) {\n promiseRef.current = result.collection.preload()\n }\n // THROW PROMISE - React Suspense catches this (React 18+ required)\n // Note: We don't check React version here. In React <18, this will be caught\n // by an Error Boundary, which provides a reasonable failure mode.\n throw promiseRef.current\n }\n\n // Return data without status/loading flags (handled by Suspense/ErrorBoundary)\n // If error after success, return last known good state (stale data)\n return {\n state: result.state,\n data: result.data,\n collection: result.collection,\n }\n}\n"],"names":["useRef","useLiveQuery"],"mappings":";;;;AAyHO,SAAS,qBACd,2BACA,OAAuB,IACvB;AACA,QAAM,aAAaA,MAAAA,OAA6B,IAAI;AACpD,QAAM,gBAAgBA,MAAAA,OAAyC,IAAI;AACnE,QAAM,kBAAkBA,MAAAA,OAAO,KAAK;AAGpC,QAAM,SAASC,aAAAA,aAAa,2BAA2B,IAAI;AAG3D,MAAI,cAAc,YAAY,OAAO,YAAY;AAC/C,eAAW,UAAU;AACrB,kBAAc,UAAU,OAAO;AAC/B,oBAAgB,UAAU;AAAA,EAC5B;AAGA,MAAI,OAAO,WAAW,SAAS;AAC7B,oBAAgB,UAAU;AAC1B,eAAW,UAAU;AAAA,EACvB;AAIA,MAAI,CAAC,OAAO,WAAW;AAErB,UAAM,IAAI;AAAA,MACR;AAAA,IAAA;AAAA,EAEJ;AAIA,MAAI,OAAO,WAAW,WAAW,CAAC,gBAAgB,SAAS;AACzD,eAAW,UAAU;AAGrB,UAAM,IAAI,MAAM,eAAe,OAAO,WAAW,EAAE,kBAAkB;AAAA,EACvE;AAEA,MAAI,OAAO,WAAW,aAAa,OAAO,WAAW,QAAQ;AAE3D,QAAI,CAAC,WAAW,SAAS;AACvB,iBAAW,UAAU,OAAO,WAAW,QAAA;AAAA,IACzC;AAIA,UAAM,WAAW;AAAA,EACnB;AAIA,SAAO;AAAA,IACL,OAAO,OAAO;AAAA,IACd,MAAM,OAAO;AAAA,IACb,YAAY,OAAO;AAAA,EAAA;AAEvB;;"}