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..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,6 +95,8 @@ Derived expressions are recalculated when their dependencies change, but you can > [!NOTE] Prior to Svelte 5.25, deriveds were read-only. +> [!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 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..7a9d620c1f 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` 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 89e1c9d718..baddff37a8 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` 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 > 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 d555b6bb1d..ad2034b45c 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 20bef78306..23e82f8fa9 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 @@ -505,6 +506,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. *