Svelte 5 $state Stale UI: Diagnose In-Place Mutation Reactivity Loss
UI stays stale after mutating a $state object or array in Svelte 5. A diagnostic guide for recognizing proxy reactivity loss, checking $state usage, and fixing with reassignment instead of in-place mutation.
08 Dec 2025, 19:29 UTC

Problem: UI stays stale after mutating a $state object or array
In Svelte 5 runes mode the UI does not update after code like arr.push(x), obj.key = v or a nested property change. There are no console errors and the template reads the same value on the next render. The useful takeaway is that $state is a proxy. Reactivity is guaranteed on assignment to the proxied root and on tracked reads/writes through the proxy. In-place mutations can change the underlying value without triggering the proxy trap that schedules an update.
Recognizable condition
- Component declares state with
let x = $state(...)and runs on Svelte 5 runtime. - Mutation occurs in an event handler or effect:
items.push(item),user.name = 'A',obj.nested.flag = true. - Template shows the old value. Logging the state immediately after mutation shows the new value in JS, but the DOM does not change.
- No errors, no warnings. $derived values that depend on the mutated field also stay stale.
Cause / diagnostic table
| Symptom | Likely cause | Quick test |
|---|---|---|
| Array push/splice does not re-render | In-place mutation of $state array proxy | Replace with reassignment and observe template change |
| Object property set does not re-render | Write to nested field without reassigning root proxy | Read the field in template, then reassign root with spread |
| $derived stays stale | Derived reads a cached snapshot, not the live proxy | Move computation into $derived and reference $state directly |
| $effect does not re-run | Dependency set unchanged because proxy trap not triggered | Log dependencies inside effect after mutation |
| Works in Svelte 4 but not Svelte 5 | Mixing stores/reactive statements with runes | Check for $: statements and store subscriptions |
Ordered checks
1. Confirm runes usage and proxy target
Inspect the component file. Verify the state is declared with the rune and not with Svelte 4 patterns.
let items = $state([]);
let user = $state({ name: '' });
Check that mutations target the variable declared with $state, not a detached copy obtained before the proxy was created. A detached copy will mutate plain JS and never notify the runtime.
2. Reproduce with a minimal mutation
Create a minimal Svelte 5 component that isolates the mutation. Log the value before and after mutation and check whether a template read changes.
<script>
let obj = $state({ n: 0 });
function incInPlace() { obj.n++; }
function incReassign() { obj = { ...obj, n: obj.n + 1 }; }
</script>
<p>{obj.n}</p>
If the template updates only on reassignment, the proxy is not being triggered by the in-place write.
3. Audit $derived and $effect reads
Ensure derived values read the $state directly in their expression, not a value captured earlier.
let count = $state(0);
let doubled = $derived(count * 2);
Avoid manual subscriptions or storing a reference to a nested object outside the proxy and then mutating that reference.
Fixes tied to findings
In-place array mutation
Replace mutation with an immutable update that reassigns the $state root.
// before
items.push(x);
// after
items = [...items, x];
// or
items = items.toSpliced(items.length, 0, x);
In-place object property change
Reassign the top-level $state object. For deep updates, reassign the root and rebuild the path.
// before
user.name = 'A';
// after
user = { ...user, name: 'A' };
For nested structures: user = { ...user, profile: { ...user.profile, flag: true } };
Derived and effect tracking
Move computation into $derived and ensure $effect accesses the $state value directly so the dependency set is recorded on the proxy.
let total = $derived(items.reduce((a,b) => a + b, 0));
Avoid caching a snapshot like const snap = items and then mutating snap.
Escalation criteria
- Reassignment fixes the UI but causes noticeable jank on large arrays. Consider splitting state into smaller $state units or normalizing data.
- Reactivity loss persists across component boundaries where state is passed as props. Audit the prop boundary for runes vs stores and ensure the parent reassigns the prop value.
- Issue appears only in SvelteKit SSR/hydration. Test the component in client-only mode with
ssr:falseto isolate hydration masking. If mismatch remains, audit runes boundaries and server/client state initialization.
Limitations and verification
Proxy tracking triggers on property access and assignment at the proxy level. Mutations on objects retrieved before proxy creation are not tracked. Deep reactivity is not automatic for all nested structures; reassigning the root $state is the supported way to guarantee an update.
Practical check: after applying a fix, trigger the mutation and confirm the template re-renders and $derived updates. In SvelteKit, verify the same behavior in client-only mode to rule out hydration effects. Behavior is version-sensitive: Svelte 4 reactivity used stores and reactive statements, Svelte 5 uses runes. Mixing patterns causes silent failures.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.