fix: warn when reassigning derived state on server and document client/server divergence

- 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
pull/18686/head
marwan562 3 weeks ago
parent 15720b16a5
commit f6a9f48436

@ -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)

@ -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. > [!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 ## 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==)... 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==)...

@ -1,5 +1,13 @@
<!-- This file is generated by scripts/process-messages/index.js. Do not edit! --> <!-- This file is generated by scripts/process-messages/index.js. Do not edit! -->
### 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 ### unresolved_hydratable
``` ```

@ -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 ## unresolved_hydratable
> A `hydratable` value with key `%key%` was created, but at least part of it was not used during the render. > A `hydratable` value with key `%key%` was created, but at least part of it was not used during the render.

@ -102,9 +102,9 @@ function build_assignment(operator, left, right, context) {
const binding = context.state.scope.get(object.name); const binding = context.state.scope.get(object.name);
// TODO 6.0 this won't work perfectly: once a derived is written to, it will // Derived reassignment on the server is permanent (no reactive graph).
// no longer recompute. It might be better to disallow writing to deriveds // We now warn in DEV via `derived_reassignment` in `src/internal/server/index.js`
// on the server, to prevent this bug occurring // See sveltejs/svelte#18681. TODO 6.0: consider disallowing writes to deriveds on server.
if (binding?.kind === 'derived' && object === left) { if (binding?.kind === 'derived' && object === left) {
let value = /** @type {Expression} */ ( let value = /** @type {Expression} */ (
context.visit(build_assignment_value(operator, left, right)) context.visit(build_assignment_value(operator, left, right))

@ -23,6 +23,7 @@ import {
} from '../../utils.js'; } from '../../utils.js';
import { Renderer } from './renderer.js'; import { Renderer } from './renderer.js';
import * as e from './errors.js'; import * as e from './errors.js';
import * as w from './warnings.js';
import { ssr_context } from './context.js'; import { ssr_context } from './context.js';
// https://html.spec.whatwg.org/multipage/syntax.html#attributes-2 // https://html.spec.whatwg.org/multipage/syntax.html#attributes-2
@ -498,6 +499,8 @@ export function derived(fn) {
return updated_value ?? get_value(); return updated_value ?? get_value();
} }
if (DEV) w.derived_reassignment();
updated_value = new_value; updated_value = new_value;
return updated_value; return updated_value;
}; };

@ -5,6 +5,17 @@ import { DEV } from 'esm-env';
var bold = 'font-weight: bold'; var bold = 'font-weight: bold';
var normal = 'font-weight: normal'; 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. * A `hydratable` value with key `%key%` was created, but at least part of it was not used during the render.
* *

Loading…
Cancel
Save