Diagnosing Svelte UI Not Updating After State Changes
When a Svelte variable changes but the DOM stays static, the issue is usually missing assignment-based invalidation. Learn how to diagnose and fix it in Svelte 3/4 and note differences in Svelte 5.
22 Aug 2026, 04:28 UTC

Recognizable Condition
You see a variable change in your Svelte component — for example, an array grows with push, a counter increments in a helper, or an object property is set — but the DOM or a $: block never re‑runs. The UI appears frozen even though the code logic runs.
Cause and Diagnostic Table
| Symptom | Likely Cause |
|---|---|
| List doesn't grow after .push(item) | Mutation without reassignment |
| $: block doesn't run despite variable change | Dependency is not statically tracked in the block |
| Store doesn't update in manual subscriptions | Stale closure or lack of immutable update |
| Code works in Svelte 5 but fails in 4 | Incompatible reactivity semantics |
Ordered Checks
- Verify Svelte version
Open package.json and check the svelte version. If it starts with 4. or 3., you are using compile‑time assignment‑based reactivity. If it is 5., reactivity is handled by runes ($state, $derived, $effect) and the rules below do not apply.
- Look for mutation vs. assignment
Search for methods that change the contents of an array or object without reassigning the variable (push, splice, obj.prop = value). In Svelte 3/4 the compiler only invalidates when it sees an assignment (=) to the variable.
Fix: Replace the mutation with an immutable update or reassign after mutation.
// Mutation (no update) items.push('New'); // Fix A: spread operator (creates new reference) items = [...items, 'New']; // Fix B: reassign after mutation items.push('New'); items = items; - Inspect $: block dependencies
Reactive statements track dependencies by scanning the block for direct variable references. If a variable is only used inside a function call, Svelte may not see it as a dependency.
Problem example:
let count = 0; $: console.log(format(count)); // count may not be tracked function format(v) { return `Value: ${v}`; }Fix: Reference the variable directly or pass it as an argument and also reference it.
$: { console.log(format(count)); count; // forces tracking } // or $: console.log(format(count)); // and ensure format uses the argument only - Review store usage
In templates, $store subscribes automatically. In a manual store.subscribe callback, mutating an object inside the callback suffers from the same mutation‑vs‑assignment issue.
Fix: Update the store immutably so a new object reference is produced.
// Wrong: mutating the current value store.subscribe(value => { value.items.push('New'); // store still holds same reference }); // Correct: produce a new object store.update(n => ({ ...n, items: [...n.items, 'New'] }));
Escalation Criteria
If the above checks do not restore updates, consider:
- Keyed #each blocks with duplicate keys causing stale items.
- Component props not being passed down (prop shielding).
- Third‑party libraries mutating Svelte‑tracked objects outside the component’s scope.
- Accidental closure capture (e.g., setInterval holding an old variable); move the logic inside a reactive block or read from a store via get.
Practical Verification
After applying a fix, add a temporary console.log inside the reactive statement or store update to confirm it re‑runs:
$: {
console.log('Reactive block ran, count =', count);
}
Open the browser console and interact with the UI; the log should appear each time the expected state change occurs.
Limitations
Reassigning large arrays or objects for reactivity can create garbage‑collection pressure in hot loops. For frequent updates, prefer a Svelte store with $state (Svelte 5) or immutable update patterns that change only the needed slice, or use $$props‑level techniques outside the scope of this guide.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.