Skip to content

Commit 9e4cbef

Browse files
KyleAMathewsclaude
andauthored
Document how to destructure in Svelte (#733)
* docs(svelte-db): Add documentation for destructuring reactivity issue (#414) ## Summary This commit addresses issue #414 where users reported that destructuring the return value from useLiveQuery() breaks reactivity in Svelte 5. ## Root Cause This is a fundamental limitation of Svelte 5's reactivity system, not a bug in the library. When objects with getters are destructured, the destructuring evaluates getters once and captures the values at that moment, losing the reactive connection. ## Solution Added comprehensive documentation explaining: - Why direct destructuring breaks reactivity - Two correct usage patterns: 1. Use dot notation (recommended): `query.data`, `query.isLoading` 2. Wrap with $derived: `const { data } = $derived(query)` ## Changes - Updated JSDoc comments in useLiveQuery.svelte.ts with detailed explanation and examples - Updated README.md with clear usage guidelines - Added test case demonstrating the correct $derived pattern - All 23 existing tests continue to pass ## References - Issue: #414 - Svelte documentation: sveltejs/svelte#11002 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> * chore(svelte-db): Revert README changes to keep it minimal The README is intentionally kept small, so reverting the detailed documentation. The comprehensive documentation remains in the JSDoc comments in useLiveQuery.svelte.ts. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> * chore: Remove package-lock.json (project uses pnpm) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> --------- Co-authored-by: Claude <[email protected]>
1 parent 7e9a1d8 commit 9e4cbef

File tree

2 files changed

+124
-1
lines changed

2 files changed

+124
-1
lines changed

packages/svelte-db/src/useLiveQuery.svelte.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,13 +68,38 @@ function toValue<T>(value: MaybeGetter<T>): T {
6868
* @param queryFn - Query function that defines what data to fetch
6969
* @param deps - Array of reactive dependencies that trigger query re-execution when changed
7070
* @returns Reactive object with query data, state, and status information
71+
*
72+
* @remarks
73+
* **IMPORTANT - Destructuring in Svelte 5:**
74+
* Direct destructuring breaks reactivity. To destructure, wrap with `$derived`:
75+
*
76+
* ❌ **Incorrect** - Loses reactivity:
77+
* ```ts
78+
* const { data, isLoading } = useLiveQuery(...)
79+
* ```
80+
*
81+
* ✅ **Correct** - Maintains reactivity:
82+
* ```ts
83+
* // Option 1: Use dot notation (recommended)
84+
* const query = useLiveQuery(...)
85+
* // Access: query.data, query.isLoading
86+
*
87+
* // Option 2: Wrap with $derived for destructuring
88+
* const query = useLiveQuery(...)
89+
* const { data, isLoading } = $derived(query)
90+
* ```
91+
*
92+
* This is a fundamental Svelte 5 limitation, not a library bug. See:
93+
* https://github.com/sveltejs/svelte/issues/11002
94+
*
7195
* @example
72-
* // Basic query with object syntax
96+
* // Basic query with object syntax (recommended pattern)
7397
* const todosQuery = useLiveQuery((q) =>
7498
* q.from({ todos: todosCollection })
7599
* .where(({ todos }) => eq(todos.completed, false))
76100
* .select(({ todos }) => ({ id: todos.id, text: todos.text }))
77101
* )
102+
* // Access via: todosQuery.data, todosQuery.isLoading, etc.
78103
*
79104
* @example
80105
* // With reactive dependencies
@@ -86,6 +111,14 @@ function toValue<T>(value: MaybeGetter<T>): T {
86111
* )
87112
*
88113
* @example
114+
* // Destructuring with $derived (if needed)
115+
* const query = useLiveQuery((q) =>
116+
* q.from({ todos: todosCollection })
117+
* )
118+
* const { data, isLoading, isError } = $derived(query)
119+
* // Now data, isLoading, and isError maintain reactivity
120+
*
121+
* @example
89122
* // Join pattern
90123
* const issuesQuery = useLiveQuery((q) =>
91124
* q.from({ issues: issueCollection })

packages/svelte-db/tests/useLiveQuery.svelte.test.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,96 @@ describe(`Query Collections`, () => {
116116
})
117117
})
118118

119+
it(`should maintain reactivity when destructuring return values with $derived`, () => {
120+
const collection = createCollection(
121+
mockSyncCollectionOptions<Person>({
122+
id: `test-persons-destructure`,
123+
getKey: (person: Person) => person.id,
124+
initialData: initialPersons,
125+
})
126+
)
127+
128+
cleanup = $effect.root(() => {
129+
// IMPORTANT: In Svelte 5, destructuring breaks reactivity unless wrapped in $derived
130+
// This is the correct pattern for destructuring (Issue #414)
131+
const query = useLiveQuery((q) =>
132+
q
133+
.from({ persons: collection })
134+
.where(({ persons }) => gt(persons.age, 30))
135+
.select(({ persons }) => ({
136+
id: persons.id,
137+
name: persons.name,
138+
age: persons.age,
139+
}))
140+
)
141+
142+
// Destructure using $derived to maintain reactivity
143+
const { data, state, isReady, isLoading } = $derived(query)
144+
145+
flushSync()
146+
147+
// Initial state checks
148+
expect(isReady).toBe(true)
149+
expect(isLoading).toBe(false)
150+
expect(state.size).toBe(1)
151+
expect(data).toHaveLength(1)
152+
expect(data[0]).toMatchObject({
153+
id: `3`,
154+
name: `John Smith`,
155+
age: 35,
156+
})
157+
158+
// Add a new person that matches the filter
159+
collection.utils.begin()
160+
collection.utils.write({
161+
type: `insert`,
162+
value: {
163+
id: `4`,
164+
name: `Alice Johnson`,
165+
age: 40,
166+
167+
isActive: true,
168+
team: `team1`,
169+
},
170+
})
171+
collection.utils.commit()
172+
173+
flushSync()
174+
175+
// Verify destructured values are still reactive after collection change
176+
expect(state.size).toBe(2)
177+
expect(data).toHaveLength(2)
178+
expect(data.some((p) => p.id === `4`)).toBe(true)
179+
expect(data.some((p) => p.id === `3`)).toBe(true)
180+
181+
// Remove a person
182+
collection.utils.begin()
183+
collection.utils.write({
184+
type: `delete`,
185+
value: {
186+
id: `3`,
187+
name: `John Smith`,
188+
age: 35,
189+
190+
isActive: true,
191+
team: `team1`,
192+
},
193+
})
194+
collection.utils.commit()
195+
196+
flushSync()
197+
198+
// Verify destructured values still track changes
199+
expect(state.size).toBe(1)
200+
expect(data).toHaveLength(1)
201+
expect(data[0]).toMatchObject({
202+
id: `4`,
203+
name: `Alice Johnson`,
204+
age: 40,
205+
})
206+
})
207+
})
208+
119209
it(`should be able to query a collection with live updates`, () => {
120210
const collection = createCollection(
121211
mockSyncCollectionOptions<Person>({

0 commit comments

Comments
 (0)