Diagnosing Stale UI in Svelte Components After Data Mutations
Learn how to identify and fix stale UI in Svelte after data mutations, with separate steps for legacy reactive statements and Svelte 5 runes mode.
23 May 2026, 23:26 UTC

Recognizable Condition
After an event handler runs, a part of the template shows an old value while other sections update correctly. The stale region refreshes only after an unrelated reassignment elsewhere in the component. No error appears in the console.
Cause/Diagnostic Table
| Observed Symptom | Likely Cause (Legacy Svelte 3/4) | Likely Cause (Svelte 5 Runes) |
|---|---|---|
Stale after arr.push(x) or arr.splice | Missing reassignment (items = items) after mutation; compiler did not invalidate reactive statements. | Source array is not a $state proxy, so mutations are not tracked. |
Stale only inside an {#each} block without a :key | Block reuse due to missing key; invalidation may have fired but DOM not recreated. | Same as legacy – key missing prevents proper re‑render. |
| Stale after a prop change | Child component reassigned the prop instead of emitting an event or using $bindable. | Child reassigned a prop (illegal in runes) or used a plain object not wrapped in $state. |
Ordered Checks
- Identify the mode – Check
package.jsonfor Svelte version and look for<svelte:options runes />or compiler config. Legacy components use$:statements; runes components use$state/$derived. - List every assignment that should feed the stale expression – In the component script, find all variables referenced by the stale
$:block or$derivedexpression. - Verify each mutation – For each listed variable, confirm that any array/object mutation (
push,splice, property assignment) is immediately followed by a reassignment (items = items) or that the variable is declared with$state. - Check dependency tracking – Ensure the reactive statement or
$derivedreferences only top‑level identifiers; nested property access likeitems.lengthis fine, butitems[0].valuewithout reassignment ofitems[0]will not trigger. - Store subscriptions – If using a store, verify it is accessed via
$store(auto‑subscribe) or that manual subscriptions are cleaned up inonDestroy. - Add keys – Insert a stable
:keyon each{#each}block and retest. - Trace invalidation – Temporarily add a
console.log inside the stale$:block or use$inspect(runes) to see when the block runs.
Fixes Tied to Findings
Legacy Svelte 3/4
- Reassign after mutation:
function addItem() {
items.push(newItem);
items = items; // triggers $: total = items.length
}
function addItem() {
items = [...items, newItem];
}
Svelte 5 Runes
- Declare reactive state with
$state:
let items = $state([]);
let total = $derived(() => items.length);
$bindable:export let items = $state([]);
// child receives items and calls add(item) prop
{#each items as item (item.id)}
{item.name}
{/each}
let external = $state(thirdPartyLib.getObject());
Escalation Criteria
Escalate to the Svelte team or file an issue when:
- All reassignment and
$statechecks are correct, yet UI remains stale. - You suspect untracked module‑level state, a third‑party mutable object not wrapped in
$state, or mixed legacy/runes compilation in the same file. - A clean rebuild (clearing
.svelte-kit, Vite cache, ornode_modules/.vite) does not change the behavior.
Before escalating, produce a minimal reproduction that isolates the mutation and the stale expression, then inspect the compiled component output (look for invalidate calls) to confirm whether the compiler generated invalidation code.
Limitations and Practical Verification
This guide cannot detect cases where a library mutates an object through a hidden reference that never touches a component‑level variable. To verify your fix:
- Add a temporary
console.log('recompute')inside the stale$:block or$derived. - Run the mutation handler and confirm the log appears.
- Check that the DOM updates accordingly.
If the log never appears, the invalidation chain is still broken; revisit the checks above.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.