diff --git a/packages/svelte-db/src/useLiveQuery.svelte.ts b/packages/svelte-db/src/useLiveQuery.svelte.ts index 4c3625fdc..c9ae8d474 100644 --- a/packages/svelte-db/src/useLiveQuery.svelte.ts +++ b/packages/svelte-db/src/useLiveQuery.svelte.ts @@ -68,13 +68,38 @@ function toValue(value: MaybeGetter): T { * @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 + * // 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 @@ -86,6 +111,14 @@ function toValue(value: MaybeGetter): T { * ) * * @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 }) diff --git a/packages/svelte-db/tests/useLiveQuery.svelte.test.ts b/packages/svelte-db/tests/useLiveQuery.svelte.test.ts index 5916c9da0..cd48a248a 100644 --- a/packages/svelte-db/tests/useLiveQuery.svelte.test.ts +++ b/packages/svelte-db/tests/useLiveQuery.svelte.test.ts @@ -116,6 +116,96 @@ describe(`Query Collections`, () => { }) }) + it(`should maintain reactivity when destructuring return values with $derived`, () => { + const collection = createCollection( + mockSyncCollectionOptions({ + id: `test-persons-destructure`, + getKey: (person: Person) => person.id, + initialData: initialPersons, + }) + ) + + cleanup = $effect.root(() => { + // IMPORTANT: In Svelte 5, destructuring breaks reactivity unless wrapped in $derived + // This is the correct pattern for destructuring (Issue #414) + const query = useLiveQuery((q) => + q + .from({ persons: collection }) + .where(({ persons }) => gt(persons.age, 30)) + .select(({ persons }) => ({ + id: persons.id, + name: persons.name, + age: persons.age, + })) + ) + + // Destructure using $derived to maintain reactivity + const { data, state, isReady, isLoading } = $derived(query) + + flushSync() + + // Initial state checks + expect(isReady).toBe(true) + expect(isLoading).toBe(false) + expect(state.size).toBe(1) + expect(data).toHaveLength(1) + expect(data[0]).toMatchObject({ + id: `3`, + name: `John Smith`, + age: 35, + }) + + // Add a new person that matches the filter + collection.utils.begin() + collection.utils.write({ + type: `insert`, + value: { + id: `4`, + name: `Alice Johnson`, + age: 40, + email: `alice.johnson@example.com`, + isActive: true, + team: `team1`, + }, + }) + collection.utils.commit() + + flushSync() + + // Verify destructured values are still reactive after collection change + expect(state.size).toBe(2) + expect(data).toHaveLength(2) + expect(data.some((p) => p.id === `4`)).toBe(true) + expect(data.some((p) => p.id === `3`)).toBe(true) + + // Remove a person + collection.utils.begin() + collection.utils.write({ + type: `delete`, + value: { + id: `3`, + name: `John Smith`, + age: 35, + email: `john.smith@example.com`, + isActive: true, + team: `team1`, + }, + }) + collection.utils.commit() + + flushSync() + + // Verify destructured values still track changes + expect(state.size).toBe(1) + expect(data).toHaveLength(1) + expect(data[0]).toMatchObject({ + id: `4`, + name: `Alice Johnson`, + age: 40, + }) + }) + }) + it(`should be able to query a collection with live updates`, () => { const collection = createCollection( mockSyncCollectionOptions({