sveltejs / svelte · Issue No. 18565
The current Svelte 5 documentation for both $derived and $effect explicitly points out a limitation in signal-based reactivity (under "Understanding dependencies"):
"Values that are read asynchronously — after an await or inside a setTimeout, for example — will not be tracked."
However, the documentation does not show developers the way to handle scenarios where they do need those values tracked. When migrating from Svelte 4 (where the compiler tracked dependencies inside async closures automatically via $: magic) to Svelte 5, developers may hit this wall when working with deferred closures such as with Dexie's liveQuery.
For instance, to convert this example in Dexie's outdated Svelte tutorial (Svelte 3/4):
$: friends = liveQuery(async () => {
// Query Dexie's API
const friends = await db.friends
.where('age')
.between(minAge, maxAge)
.toArray();
// Return result
return friends;
});
to Svelte 5, explicitly reading of those reactive states is needed:
const friends = $derived.by(() => {
// Read synchronously so Svelte tracks them
void minAge, maxAge;
return liveQuery(async () => {
// Query Dexie's API
return await db.friends
.where('age')
.between(minAge, maxAge)
.toArray();
});
});
It would be incredibly helpful to add a short section or code snippet showing the standard way to bridge Svelte's synchronous reactivity with deferred closures by capturing the state synchronously before the async boundary.
For example, in the $derived section, adding a part showing how to handle deferred callbacks like liveQuery or observables:
let query = $derived.by(() => {
// Read `searchTerm` synchronously
void searchTerm;
return createObservable(async () => {
return await fetchResults(searchTerm);
});
});
Similarly, in the $effect section, adding a workaround after the current setTimeout example:
$effect(() => {
// ...canvas setup...
// Read `size` synchronously so Svelte registers it as a dependency
void size;
const timeoutId = setTimeout(() => {
context.fillRect(0, 0, size, size);
}, 0);
return () => {
clearTimeout(timeoutId);
};
});
Third-party utilities exist for this (like watch() from the runed library), but it's only an explicit-tracking counterpart to $effect (there's not one for $derived), and official docs ought to stick to vanilla Svelte 5 without adding extra dependencies.
Additional context
I ran into this while updating the official Dexie.js Svelte tutorial to Svelte 5. I previously brought this up in discussion https://github.com/sveltejs/svelte/discussions/18545, on Stack Overflow and Discord. The consensus is that while using void state; works perfectly to register the dependency, people hesitate to call it the correct approach until it is officially documented by the Svelte team.
Having this explicitly addressed in the docs would prevent developers from guessing or relying on third-party libraries to sync state across async boundaries.
I am happy to submit a PR for this if the team agrees with adding this clarification and feels that this contribution to the official docs is welcome.
nice to have
Relay reads this issue against the repository's contribution signals: the files it is likely to touch, how the maintainers triage work this size, and what the first contribution would exercise.
The full analysis for this issue is still being assembled. Until then, the description above and the thread on GitHub are the most reliable context.