From f6a9f48436d093c8e471174ee4da634ec8743abf Mon Sep 17 00:00:00 2001 From: marwan562 Date: Sat, 22 Aug 2026 01:22:51 +0300 Subject: [PATCH 1/2] fix: warn when reassigning derived state on server and document client/server divergence MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - deriveds created with let and reassigned are temporary on client (reset when deps change via reactive graph) but permanent on server (memoized once() closure, updated_value never cleared) — see sveltejs/svelte#18681 - add DEV warning derived_reassignment in src/internal/server/index.js - add server-warnings message and regenerate warnings.js - update docs/02-runes/03-$derived.md with WARNING admonition linking #18681 - update compiler TODO comment in server AssignmentExpression visitor - changeset patch Fixes #18681 --- .changeset/derived-override-server-warning.md | 5 +++++ documentation/docs/02-runes/03-$derived.md | 2 ++ .../docs/98-reference/.generated/server-warnings.md | 8 ++++++++ packages/svelte/messages/server-warnings/warnings.md | 6 ++++++ .../server/visitors/AssignmentExpression.js | 6 +++--- packages/svelte/src/internal/server/index.js | 3 +++ packages/svelte/src/internal/server/warnings.js | 11 +++++++++++ 7 files changed, 38 insertions(+), 3 deletions(-) create mode 100644 .changeset/derived-override-server-warning.md diff --git a/.changeset/derived-override-server-warning.md b/.changeset/derived-override-server-warning.md new file mode 100644 index 0000000000..d938c0f3e9 --- /dev/null +++ b/.changeset/derived-override-server-warning.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: warn when reassigning derived state on server — overrides are permanent unlike client where they reset when dependencies change. Document server/client divergence in $derived docs (sveltejs/svelte#18681) diff --git a/documentation/docs/02-runes/03-$derived.md b/documentation/docs/02-runes/03-$derived.md index f85ba90baa..d7fda2283f 100644 --- a/documentation/docs/02-runes/03-$derived.md +++ b/documentation/docs/02-runes/03-$derived.md @@ -93,6 +93,8 @@ Derived expressions are recalculated when their dependencies change, but you can > [!NOTE] Prior to Svelte 5.25, deriveds were read-only. +> [!WARNING] Overrides behave differently between client and server. On the client, an override is temporary — it is discarded as soon as any of the derived's dependencies change, because `derived` values are tracked via the reactive graph (`push-pull` reactivity). On the server, there is no reactive graph: `svelte/server` memoizes the derived with `once(fn)` (`packages/svelte/src/internal/server/index.js:441`) and stores the override in a closure (`updated_value ?? get_value()` at `index.js:498`). Once you assign to a `derived` on the server it will **never recompute** and the override survives forever. Avoid reassigning `let`-deriveds during SSR, and be aware that shared `*.svelte.ts` modules executed in `node`/`vitest` (server codegen) will show `client: 20` vs `server: 999` in the same test — see `sveltejs/svelte#18681`. In `DEV` the server now warns when you write to a derived. + ## Deriveds and reactivity Unlike `$state`, which converts objects and arrays to [deeply reactive proxies]($state#Deep-state), `$derived` values are left as-is. For example, [in a case like this](/playground/untitled#H4sIAAAAAAAAE4VU22rjMBD9lUHd3aaQi9PdstS1A3t5XvpQ2Ic4D7I1iUUV2UjjNMX431eS7TRdSosxgjMzZ45mjt0yzffIYibvy0ojFJWqDKCQVBk2ZVup0LJ43TJ6rn2aBxw-FP2o67k9oCKP5dziW3hRaUJNjoYltjCyplWmM1JIIAn3FlL4ZIkTTtYez6jtj4w8WwyXv9GiIXiQxLVs9pfTMR7EuoSLIuLFbX7Z4930bZo_nBrD1bs834tlfvsBz9_SyX6PZXu9XaL4gOWn4sXjeyzftv4ZWfyxubpzxzg6LfD4MrooxELEosKCUPigQCMPKCZh0OtQE1iSxcsmdHuBvCiHZXALLXiN08EL3RRkaJ_kDVGle0HcSD5TPEeVtj67O4Nrg9aiSNtBY5oODJkrL5QsHtN2cgXp6nSJMWzpWWGasdlsGEMbzi5jPr5KFr0Ep7pdeM2-TCelCddIhDxAobi1jqF3cMaC1RKp64bAW9iFAmXGIHfd4wNXDabtOLN53w8W53VvJoZLh7xk4Rr3CoL-UNoLhWHrT1JQGcM17u96oES5K-kc2XOzkzqGCKL5De79OUTyyrg1zgwXsrEx3ESfx4Bz0M5UjVMHB24mw9SuXtXFoN13fYKOM1tyUT3FbvbWmSWCZX2Er-41u5xPoml45svRahl9Wb9aasbINJixDZwcPTbyTLZSUsAvrg_cPuCR7s782_WU8343Y72Qtlb8OYatwuOQvuN13M_hJKNfxann1v1U_B1KZ_D_mzhzhz24fw85CSz2irtN9w9HshBK7AQAAA==)... diff --git a/documentation/docs/98-reference/.generated/server-warnings.md b/documentation/docs/98-reference/.generated/server-warnings.md index c4a7fbefef..23cd5e5922 100644 --- a/documentation/docs/98-reference/.generated/server-warnings.md +++ b/documentation/docs/98-reference/.generated/server-warnings.md @@ -1,5 +1,13 @@ +### derived_reassignment + +``` +Assignment to derived state on the server is permanent and will not be recalculated when its dependencies change, unlike on the client where it is temporary +``` + +This warning is emitted when you reassign a value created with `$derived` while compiling with `generate: 'server'`. On the client such overrides are temporary and discarded once dependencies change; on the server the derived is memoized with `once(fn)` and the override is stored in a closure (`updated_value`), so it survives forever. See `sveltejs/svelte#18681` for details. + ### unresolved_hydratable ``` diff --git a/packages/svelte/messages/server-warnings/warnings.md b/packages/svelte/messages/server-warnings/warnings.md index 89e1c9d718..07e2db90f4 100644 --- a/packages/svelte/messages/server-warnings/warnings.md +++ b/packages/svelte/messages/server-warnings/warnings.md @@ -1,3 +1,9 @@ +## derived_reassignment + +> Assignment to derived state on the server is permanent and will not be recalculated when its dependencies change, unlike on the client where it is temporary + +This warning is emitted when you reassign a value created with `$derived` while compiling with `generate: 'server'`. On the client such overrides are temporary and discarded once dependencies change; on the server the derived is memoized with `once(fn)` and the override is stored in a closure (`updated_value`), so it survives forever. See `sveltejs/svelte#18681` for details. + ## unresolved_hydratable > A `hydratable` value with key `%key%` was created, but at least part of it was not used during the render. diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/AssignmentExpression.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/AssignmentExpression.js index e6a1a86aa8..76a2da1cdc 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/AssignmentExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/AssignmentExpression.js @@ -102,9 +102,9 @@ function build_assignment(operator, left, right, context) { const binding = context.state.scope.get(object.name); - // TODO 6.0 this won't work perfectly: once a derived is written to, it will - // no longer recompute. It might be better to disallow writing to deriveds - // on the server, to prevent this bug occurring + // Derived reassignment on the server is permanent (no reactive graph). + // We now warn in DEV via `derived_reassignment` in `src/internal/server/index.js` + // See sveltejs/svelte#18681. TODO 6.0: consider disallowing writes to deriveds on server. if (binding?.kind === 'derived' && object === left) { let value = /** @type {Expression} */ ( context.visit(build_assignment_value(operator, left, right)) diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js index 3d8ec5fe5e..978259afbb 100644 --- a/packages/svelte/src/internal/server/index.js +++ b/packages/svelte/src/internal/server/index.js @@ -23,6 +23,7 @@ import { } from '../../utils.js'; import { Renderer } from './renderer.js'; import * as e from './errors.js'; +import * as w from './warnings.js'; import { ssr_context } from './context.js'; // https://html.spec.whatwg.org/multipage/syntax.html#attributes-2 @@ -498,6 +499,8 @@ export function derived(fn) { return updated_value ?? get_value(); } + if (DEV) w.derived_reassignment(); + updated_value = new_value; return updated_value; }; diff --git a/packages/svelte/src/internal/server/warnings.js b/packages/svelte/src/internal/server/warnings.js index fc44a086af..8701c73bb5 100644 --- a/packages/svelte/src/internal/server/warnings.js +++ b/packages/svelte/src/internal/server/warnings.js @@ -5,6 +5,17 @@ import { DEV } from 'esm-env'; var bold = 'font-weight: bold'; var normal = 'font-weight: normal'; +/** + * Assignment to derived state on the server is permanent and will not be recalculated when its dependencies change, unlike on the client where it is temporary + */ +export function derived_reassignment() { + if (DEV) { + console.warn(`%c[svelte] derived_reassignment\n%cAssignment to derived state on the server is permanent and will not be recalculated when its dependencies change, unlike on the client where it is temporary\nhttps://svelte.dev/e/derived_reassignment`, bold, normal); + } else { + console.warn(`https://svelte.dev/e/derived_reassignment`); + } +} + /** * A `hydratable` value with key `%key%` was created, but at least part of it was not used during the render. * From fe46d7f3cb5debcab817c4006e22fb1b2f6f104c Mon Sep 17 00:00:00 2001 From: marwan562 Date: Sat, 22 Aug 2026 15:40:30 +0300 Subject: [PATCH 2/2] fix: simplify derived server docs per review - Remove internal implementation details (once, updated_value, push-pull) from $derived docs and server warning - Mention $derived.by alongside $derived - Remove issue link from user-facing text - Add concise no-reactivity note in first section and keep concise WARNING in Overriding section Addresses feedback from @7nik on #18686 --- documentation/docs/02-runes/03-$derived.md | 4 +++- documentation/docs/98-reference/.generated/server-warnings.md | 2 +- packages/svelte/messages/server-warnings/warnings.md | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/documentation/docs/02-runes/03-$derived.md b/documentation/docs/02-runes/03-$derived.md index d7fda2283f..2945c5be80 100644 --- a/documentation/docs/02-runes/03-$derived.md +++ b/documentation/docs/02-runes/03-$derived.md @@ -24,6 +24,8 @@ As with `$state`, you can mark class fields as `$derived`. > [!NOTE] Code in Svelte components is only executed once at creation. Without the `$derived` rune, `doubled` would maintain its original value even when `count` changes. +> [!NOTE] There is no reactivity on the server — `$derived` and `$derived.by` values do not recompute when their dependencies change, including overridden values (see [Overriding derived values](#Overriding-derived-values)). + ## `$derived.by` Sometimes you need to create complex derivations that don't fit inside a short expression. In these cases, you can use `$derived.by` which accepts a function as its argument. @@ -93,7 +95,7 @@ Derived expressions are recalculated when their dependencies change, but you can > [!NOTE] Prior to Svelte 5.25, deriveds were read-only. -> [!WARNING] Overrides behave differently between client and server. On the client, an override is temporary — it is discarded as soon as any of the derived's dependencies change, because `derived` values are tracked via the reactive graph (`push-pull` reactivity). On the server, there is no reactive graph: `svelte/server` memoizes the derived with `once(fn)` (`packages/svelte/src/internal/server/index.js:441`) and stores the override in a closure (`updated_value ?? get_value()` at `index.js:498`). Once you assign to a `derived` on the server it will **never recompute** and the override survives forever. Avoid reassigning `let`-deriveds during SSR, and be aware that shared `*.svelte.ts` modules executed in `node`/`vitest` (server codegen) will show `client: 20` vs `server: 999` in the same test — see `sveltejs/svelte#18681`. In `DEV` the server now warns when you write to a derived. +> [!WARNING] There is no reactivity on the server — when a `$derived` or `$derived.by` value's dependencies change, it will not recompute. This includes overridden values, which on the client are temporary and discarded when dependencies change, but on the server are permanent. ## Deriveds and reactivity diff --git a/documentation/docs/98-reference/.generated/server-warnings.md b/documentation/docs/98-reference/.generated/server-warnings.md index 23cd5e5922..7a9d620c1f 100644 --- a/documentation/docs/98-reference/.generated/server-warnings.md +++ b/documentation/docs/98-reference/.generated/server-warnings.md @@ -6,7 +6,7 @@ Assignment to derived state on the server is permanent and will not be recalculated when its dependencies change, unlike on the client where it is temporary ``` -This warning is emitted when you reassign a value created with `$derived` while compiling with `generate: 'server'`. On the client such overrides are temporary and discarded once dependencies change; on the server the derived is memoized with `once(fn)` and the override is stored in a closure (`updated_value`), so it survives forever. See `sveltejs/svelte#18681` for details. +This warning is emitted when you reassign a value created with `$derived` or `$derived.by` while compiling with `generate: 'server'`. There is no reactivity on the server — derived values do not recompute when dependencies change, including overridden values. On the client, overrides are temporary and discarded when dependencies change. ### unresolved_hydratable diff --git a/packages/svelte/messages/server-warnings/warnings.md b/packages/svelte/messages/server-warnings/warnings.md index 07e2db90f4..baddff37a8 100644 --- a/packages/svelte/messages/server-warnings/warnings.md +++ b/packages/svelte/messages/server-warnings/warnings.md @@ -2,7 +2,7 @@ > Assignment to derived state on the server is permanent and will not be recalculated when its dependencies change, unlike on the client where it is temporary -This warning is emitted when you reassign a value created with `$derived` while compiling with `generate: 'server'`. On the client such overrides are temporary and discarded once dependencies change; on the server the derived is memoized with `once(fn)` and the override is stored in a closure (`updated_value`), so it survives forever. See `sveltejs/svelte#18681` for details. +This warning is emitted when you reassign a value created with `$derived` or `$derived.by` while compiling with `generate: 'server'`. There is no reactivity on the server — derived values do not recompute when dependencies change, including overridden values. On the client, overrides are temporary and discarded when dependencies change. ## unresolved_hydratable