Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
35 changes: 34 additions & 1 deletion packages/svelte-db/src/useLiveQuery.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,13 +68,38 @@ function toValue<T>(value: MaybeGetter<T>): 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
Expand All @@ -86,6 +111,14 @@ function toValue<T>(value: MaybeGetter<T>): 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 })
Expand Down
90 changes: 90 additions & 0 deletions packages/svelte-db/tests/useLiveQuery.svelte.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,96 @@ describe(`Query Collections`, () => {
})
})

it(`should maintain reactivity when destructuring return values with $derived`, () => {
const collection = createCollection(
mockSyncCollectionOptions<Person>({
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<Person>({
Expand Down
Loading