Skip to content

Overridable deriveds: under server codegen an override survives dependency changes forever — the documented lifetime silently differs between the two builds of the same universal module #18681

Description

@urbicon

Describe the bug

Since 5.25, deriveds declared with let can be reassigned; the docs describe the lifetime as

Derived expressions are recalculated when their dependencies change, but you can temporarily override their values by reassigning them

For a .svelte.ts module compiled with client codegen this holds in every context I measured (owned and unowned, flat and chained). Under server codegen the documented lifetime does not exist: after the reassignment the derived is never evaluated again, so the override survives every dependency change, permanently.

// store.svelte.ts — universal code, runs during SSR and on the client
let seed = $state(1);
let d = $derived.by(() => seed * 10);

d;          // 10
d = 999;    // override
seed = 2;   // dependency change
d;          // client build: 20 (documented) · server build: 999, forever

That means a shared rune store (the "universal reactivity" use case) has two different override semantics depending on which build executes it — e.g. an optimistic-UI override applied during SSR is never corrected server-side — and nothing in the docs mentions a difference.

The practical sharp edge is that test setups fall into the server world unnoticed. With Vitest, environment: 'node' gives you the SSR transform (server codegen). And the jsdom default without resolve.conditions: ['browser'] produces a third, mixed state: client codegen but the server top-level svelte entry, where override semantics are client-like but flushSync is a silent no-op and $effect.root effects never fire. We first noticed all this as the same test sequence yielding contradictory results in two files of one repo.

Related, but not the same: #15934 / #15414 cover unowned per-read re-evaluation in the client build — that behaviour does not break the documented override lifetime; the server-codegen divergence reported here is a separate, undocumented one.

Question: is the server behaviour intended (i.e. override lifetime is only specified for the client build)? If so, could the docs state that overrides are permanent under SSR/server codegen — and should reassigning a derived in server code perhaps warn in DEV?

Reproduction

Repository-free repro (a REPL cannot show this — the playground always runs mounted client code, which is exactly the one world that matches the docs):

https://github.com/urbicon/svelte-derived-override-server-codegen — a minimal vitest project with three projects (node env → server codegen; jsdom default → client codegen + server entry; jsdom with resolve.conditions: ['browser'] → full client). bun install && bunx vitest run prints the matrix; the README carries the full result table and the ~20-line scenario module.

context codegen override after dependency change derived eval count
node env, flat server 999 — survives forever 1 (never re-evaluated after override)
node env, chained server survives (through the chain) inner 1 / outer 4
jsdom default, flat/chained/owned client (+ server entry) 20 / 21 — documented 2
jsdom + browser condition, flat/chained/owned client 20 / 21 — documented 2

Logs

No errors or warnings are emitted in any of the cases — the divergence is silent.

System Info

  • svelte 5.56.10, @sveltejs/vite-plugin-svelte 7.3.0, vitest 4.1.11, jsdom 30.0.1
  • macOS (darwin arm64), node v25.2.1

Severity

annoyance

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions