From 5e45afd48e4bb652f77111cf69a468d7f98f2974 Mon Sep 17 00:00:00 2001 From: skepticattus Date: Wed, 22 Jul 2026 23:48:12 +0700 Subject: [PATCH 1/5] docs: document pattern for explicit dependency tracking across asynchronous boundaries --- documentation/docs/02-runes/03-$derived.md | 25 ++++++++++++++++++++++ documentation/docs/02-runes/04-$effect.md | 25 ++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/documentation/docs/02-runes/03-$derived.md b/documentation/docs/02-runes/03-$derived.md index f85ba90baa..a5a778af52 100644 --- a/documentation/docs/02-runes/03-$derived.md +++ b/documentation/docs/02-runes/03-$derived.md @@ -62,6 +62,31 @@ let total = $derived(await a + b); ...both `a` and `b` are tracked, even though `b` is only read once `a` has resolved, after the initial execution. (This does not apply to `await` in functions that are called by the expression, only the expression itself.) +On the other hand, values that are read _asynchronously_ (such as inside asynchronous closures passed to third-party query or observable libraries) will not be tracked automatically. To track these values, read them synchronously outside the asynchronous boundary. A clear pattern for this is to evaluate the dependencies using the `void` operator: + +```js +import { Dexie, liveQuery } from 'dexie'; + +const db = new Dexie('UserDatabase'); +// ...database setup... + +let minAge = $state(18); +let maxAge = $state(30); + +let query = $derived.by(() => { + // Read dependencies synchronously so they are tracked + void minAge, maxAge; + + return liveQuery(async () => { + // Values read inside this callback are not tracked automatically + return await db.users + .where('age') + .between(minAge, maxAge) + .toArray(); + }); +}); +``` + To exempt a piece of state from being treated as a dependency, use [`untrack`](svelte#untrack). ## Overriding derived values diff --git a/documentation/docs/02-runes/04-$effect.md b/documentation/docs/02-runes/04-$effect.md index a13fc7bc46..b7441d1764 100644 --- a/documentation/docs/02-runes/04-$effect.md +++ b/documentation/docs/02-runes/04-$effect.md @@ -107,6 +107,31 @@ $effect(() => { }); ``` +To force the `$effect` to track the values read _asynchronously_, read them synchronously outside the asynchronous boundary. A clear pattern for this is to evaluate the dependencies using the `void` operator: + +```ts +// @filename: index.ts +declare let canvas: { + width: number; + height: number; + getContext(type: '2d', options?: CanvasRenderingContext2DSettings): CanvasRenderingContext2D; +}; +declare let size: number; + +// ---cut--- +$effect(() => { + const context = canvas.getContext('2d'); + + +++// read `size` synchronously so it is registered as a dependency+++ + +++void size;+++ + + setTimeout(() => { + // ...and now this will also re-run when `size` changes + context.fillRect(0, 0, size, size); + }, 0); +}); +``` + An effect only reruns when the object it reads changes, not when a property inside it changes. (If you want to observe changes _inside_ an object at dev time, you can use [`$inspect`]($inspect).) ```svelte From d0bd52dd2ffcd651b5f6aba123f2b146c36313e6 Mon Sep 17 00:00:00 2001 From: skepticattus Date: Thu, 23 Jul 2026 12:02:51 +0700 Subject: [PATCH 2/5] add ambient type declaration --- documentation/docs/02-runes/03-$derived.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/documentation/docs/02-runes/03-$derived.md b/documentation/docs/02-runes/03-$derived.md index a5a778af52..82d7ebda1b 100644 --- a/documentation/docs/02-runes/03-$derived.md +++ b/documentation/docs/02-runes/03-$derived.md @@ -65,6 +65,23 @@ let total = $derived(await a + b); On the other hand, values that are read _asynchronously_ (such as inside asynchronous closures passed to third-party query or observable libraries) will not be tracked automatically. To track these values, read them synchronously outside the asynchronous boundary. A clear pattern for this is to evaluate the dependencies using the `void` operator: ```js +// @filename: ambient.d.ts +declare module 'dexie' { + export class Dexie { + constructor(databaseName: string); + users: { + where(key: string): { + between(min: number, max: number): { + toArray(): Promise; + }; + }; + }; + } + export function liveQuery(querier: () => T | Promise): any; +} + +// @filename: index.js +// ---cut--- import { Dexie, liveQuery } from 'dexie'; const db = new Dexie('UserDatabase'); From 7a327fa76f210c06af3f392b07af6733c223509c Mon Sep 17 00:00:00 2001 From: skepticattus Date: Thu, 23 Jul 2026 12:17:54 +0700 Subject: [PATCH 3/5] document pattern of extracting async callback into a separate function in $derived --- documentation/docs/02-runes/03-$derived.md | 42 ++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/documentation/docs/02-runes/03-$derived.md b/documentation/docs/02-runes/03-$derived.md index 82d7ebda1b..d28db71d69 100644 --- a/documentation/docs/02-runes/03-$derived.md +++ b/documentation/docs/02-runes/03-$derived.md @@ -91,8 +91,8 @@ let minAge = $state(18); let maxAge = $state(30); let query = $derived.by(() => { - // Read dependencies synchronously so they are tracked - void minAge, maxAge; + +++// Read dependencies synchronously so they are tracked+++ + +++void minAge, maxAge;+++ return liveQuery(async () => { // Values read inside this callback are not tracked automatically @@ -104,6 +104,44 @@ let query = $derived.by(() => { }); ``` +Alternatively, you can extract the asynchronous callback into a separate function and pass the reactive values as arguments, which are evaluated synchronously: + +```js +// @filename: ambient.d.ts +declare module 'dexie' { + export class Dexie { + constructor(databaseName: string); + users: { + where(key: string): { + between(min: number, max: number): { + toArray(): Promise; + }; + }; + }; + } + export function liveQuery(querier: () => T | Promise): any; +} + +// @filename: index.js +import { Dexie, liveQuery } from 'dexie'; + +const db = new Dexie('UserDatabase'); +let minAge = $state(18); +let maxAge = $state(30); +// ---cut--- ++++function createUserQuery(minAge, maxAge) {+++ + return liveQuery(async () => { + return await db.users + .where('age') + .between(minAge, maxAge) + .toArray(); + }); +} + +// minAge and maxAge are read synchronously when the function is called +let query = $derived(+++createUserQuery(minAge, maxAge)+++); +``` + To exempt a piece of state from being treated as a dependency, use [`untrack`](svelte#untrack). ## Overriding derived values From dafab9e1a641f8f0846e28cf595740f885e8c7d1 Mon Sep 17 00:00:00 2001 From: skepticattus Date: Thu, 23 Jul 2026 13:54:17 +0700 Subject: [PATCH 4/5] document pattern of extracting async callback into a separate function in $effect --- documentation/docs/02-runes/04-$effect.md | 26 +++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/documentation/docs/02-runes/04-$effect.md b/documentation/docs/02-runes/04-$effect.md index b7441d1764..eb2b75e115 100644 --- a/documentation/docs/02-runes/04-$effect.md +++ b/documentation/docs/02-runes/04-$effect.md @@ -132,6 +132,32 @@ $effect(() => { }); ``` +Alternatively, you can extract the asynchronous logic into a separate function and pass the reactive values as arguments, which are evaluated synchronously: + +```ts +// @filename: index.ts +declare let canvas: { + width: number; + height: number; + getContext(type: '2d', options?: CanvasRenderingContext2DSettings): CanvasRenderingContext2D; +}; +declare let size: number; + +// ---cut--- +$effect(() => { + const context = canvas.getContext('2d'); + + +++const draw = (size) => {+++ + setTimeout(() => { + context.fillRect(0, 0, size, size); + }, 0); + }; + + // size is read synchronously when the function is called + +++draw(size);+++ +}); +``` + An effect only reruns when the object it reads changes, not when a property inside it changes. (If you want to observe changes _inside_ an object at dev time, you can use [`$inspect`]($inspect).) ```svelte From e202b95ef06fb492144a47281d8c6d8d83cd46ac Mon Sep 17 00:00:00 2001 From: skepticattus Date: Thu, 23 Jul 2026 13:58:44 +0700 Subject: [PATCH 5/5] add type number to extracted function --- documentation/docs/02-runes/04-$effect.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/docs/02-runes/04-$effect.md b/documentation/docs/02-runes/04-$effect.md index eb2b75e115..50817c36b0 100644 --- a/documentation/docs/02-runes/04-$effect.md +++ b/documentation/docs/02-runes/04-$effect.md @@ -147,7 +147,7 @@ declare let size: number; $effect(() => { const context = canvas.getContext('2d'); - +++const draw = (size) => {+++ + +++const draw = (size: number) => {+++ setTimeout(() => { context.fillRect(0, 0, size, size); }, 0);