Fix: Svelte Store Not Updating — Reactive Store Issues
Quick Answer
How to fix Svelte store not updating the UI — writable vs readable stores, derived stores, subscribe pattern, store mutation vs assignment, and custom store patterns.
The Problem
A Svelte store value changes but the component template doesn’t reflect the update:
// store.ts
import { writable } from 'svelte/store';
export const count = writable(0);
// component.svelte — template doesn't update
count.update(n => n + 1); // Called but template stays at 0Or a derived store doesn’t update when its source changes:
const doubled = derived(count, $count => $count * 2);
// doubled stays stale after count changesOr a store mutation (not assignment) doesn’t trigger reactivity:
// items is a writable store containing an array
items.update(arr => {
arr.push(newItem); // Mutates — sometimes doesn't trigger reactivity
return arr;
});Or the store subscription fires but the template doesn’t re-render.
Why This Happens
Svelte’s reactivity model is compile-time: the Svelte compiler transforms $store auto-subscriptions and assignments into reactive declarations. When something bypasses this mechanism, updates don’t propagate:
- Mutating store value instead of replacing it — Svelte’s store reactivity fires when
set()orupdate()is called. Insideupdate(), mutating the existing value (.push(), direct property modification) and returning the same reference may fail to trigger reactive updates in derived stores or components that compare by reference. - Not using the
$prefix — the$storeauto-subscription syntax (e.g.,$count) is compiled into a subscription/unsubscription. Usingstorewithout the$gives you the store object, not its current value. - Manual subscription without unsubscribing — calling
store.subscribe()without returning the unsubscribe function inonDestroycauses stale subscriptions and memory leaks. - Derived store dependency not tracked — derived store calculations only track dependencies accessed during the first synchronous execution. Conditional access to stores may break the dependency graph.
- Store set in a non-reactive context — setting a store inside a callback or async operation not connected to Svelte’s reactivity (in a different module, Web Worker, etc.) requires special handling.
Fix 1: Use the $store Auto-Subscription Syntax
In Svelte components, prefix the store name with $ to automatically subscribe and get the current value:
<!-- WRONG — using the store object directly, not the current value -->
<script>
import { count } from './store';
// count is the store object, not the value
</script>
<p>Count: {count}</p> <!-- Shows [object Object], not the value -->
<button on:click={() => count.update(n => n + 1)}>+</button><!-- CORRECT — use $count to access the reactive value -->
<script>
import { count } from './store';
// $count automatically subscribes and gives the current value
</script>
<p>Count: {$count}</p> <!-- Shows the actual number, updates automatically -->
<button on:click={() => count.update(n => n + 1)}>+</button>The $ prefix is only valid inside .svelte component files. In .ts or .js files, subscribe manually:
// utils.ts — no $store syntax available here
import { get } from 'svelte/store';
import { count } from './store';
// Read the current value once
const currentCount = get(count);
// Subscribe to changes
const unsubscribe = count.subscribe(value => {
console.log('count changed:', value);
});
// Always unsubscribe when done
unsubscribe();Fix 2: Use Immutable Updates in Stores
When a store holds an object or array, return a new reference from update() to ensure all subscribers and derived stores detect the change:
// store.ts
import { writable } from 'svelte/store';
export const items = writable<string[]>([]);
// WRONG — mutating in place, same array reference returned
items.update(arr => {
arr.push('new item'); // Mutates the original array
return arr; // Same reference — derived stores may not update
});
// CORRECT — return a new array
items.update(arr => [...arr, 'new item']); // New reference → all subscribers notifiedSame for objects:
export const user = writable({ name: 'Alice', age: 30 });
// WRONG — mutating the object
user.update(u => {
u.name = 'Bob'; // Mutates
return u; // Same reference
});
// CORRECT — spread to create new object
user.update(u => ({ ...u, name: 'Bob' }));For nested structures:
export const config = writable({
theme: 'light',
layout: { sidebar: true, compact: false }
});
// Update nested property immutably
config.update(c => ({
...c,
layout: { ...c.layout, compact: true }
}));Common Mistake: Using
Array.prototype.sort()orArray.prototype.reverse()in an update — these mutate in place. Use spread first:([...arr].sort(...)).
Fix 3: Fix Derived Store Dependency Tracking
derived() tracks which stores are accessed synchronously. Conditional store access breaks the dependency graph:
import { writable, derived } from 'svelte/store';
const isMetric = writable(true);
const value = writable(100);
// WRONG — derived only tracks the stores accessed in the first run
// If isMetric starts as true, 'value' is always tracked,
// but the derivation may not update correctly in all cases
const display = derived([isMetric, value], ([$isMetric, $value]) => {
return $isMetric ? `${$value} km` : `${$value * 0.621} mi`;
});
// CORRECT — always access all dependent stores in the array
// The correct pattern above actually works — pass all deps in the array
// The issue arises when using a single store arg with conditional access:
// WRONG — conditional single-store derived
const problematic = derived(isMetric, ($isMetric) => {
if ($isMetric) {
return get(value) + ' km'; // 'value' not tracked by derived!
}
return get(value) * 0.621 + ' mi';
});
// CORRECT — list all stores in the array
const correct = derived(
[isMetric, value],
([$isMetric, $value]) => $isMetric ? `${$value} km` : `${$value * 0.621} mi`
);Derived store with async updates:
// For async derived values, use the callback form
const asyncData = derived(
userId,
($userId, set) => {
set(null); // Set initial value while loading
fetch(`/api/users/${$userId}`)
.then(r => r.json())
.then(data => set(data));
// Return cleanup function
return () => {
// Cancel any in-flight requests if needed
};
},
null // Initial value
);Fix 4: Manually Subscribe with Proper Cleanup
When you need to subscribe in non-reactive code (inside onMount, event handlers, or plain TypeScript files):
<!-- WRONG — subscribe without cleanup causes memory leak and stale updates -->
<script>
import { onMount } from 'svelte';
import { data } from './store';
let currentData;
onMount(() => {
data.subscribe(value => {
currentData = value;
});
// No cleanup — subscription stays active after component is destroyed
});
</script><!-- CORRECT — return unsubscribe from onMount for auto-cleanup -->
<script>
import { onMount } from 'svelte';
import { data } from './store';
let currentData;
onMount(() => {
const unsubscribe = data.subscribe(value => {
currentData = value;
});
return unsubscribe; // Svelte calls this on component destroy
});
</script>
<!-- Or use the $store syntax which handles this automatically -->
<p>{$data}</p>In a .ts file:
// service.ts
import { data } from './store';
export function watchData() {
const unsubscribe = data.subscribe(value => {
console.log('data:', value);
});
// Call when you want to stop watching
return unsubscribe;
}Fix 5: Build Custom Stores
Custom stores expose subscribe, set, and update while adding domain-specific methods:
// cart.store.ts
import { writable, derived, get } from 'svelte/store';
interface CartItem {
id: string;
name: string;
price: number;
quantity: number;
}
function createCartStore() {
const { subscribe, set, update } = writable<CartItem[]>([]);
return {
subscribe, // Required for $store syntax and derived stores
addItem(item: Omit<CartItem, 'quantity'>) {
update(items => {
const existing = items.find(i => i.id === item.id);
if (existing) {
// Immutable update — new array and new object
return items.map(i =>
i.id === item.id
? { ...i, quantity: i.quantity + 1 }
: i
);
}
return [...items, { ...item, quantity: 1 }];
});
},
removeItem(id: string) {
update(items => items.filter(i => i.id !== id));
},
clear() {
set([]);
},
};
}
export const cart = createCartStore();
// Derived store — automatically updates when cart changes
export const cartTotal = derived(cart, $cart =>
$cart.reduce((sum, item) => sum + item.price * item.quantity, 0)
);
export const cartCount = derived(cart, $cart =>
$cart.reduce((sum, item) => sum + item.quantity, 0)
);<!-- CartComponent.svelte -->
<script>
import { cart, cartTotal, cartCount } from './cart.store';
</script>
<p>Items: {$cartCount}</p>
<p>Total: ${$cartTotal.toFixed(2)}</p>
{#each $cart as item}
<div>
{item.name} × {item.quantity}
<button on:click={() => cart.removeItem(item.id)}>Remove</button>
</div>
{/each}Fix 6: Persist Stores to localStorage
For stores that should survive page reloads:
// persistent.store.ts
import { writable } from 'svelte/store';
export function persistentWritable<T>(key: string, initial: T) {
// Read initial value from localStorage if available
const stored = typeof localStorage !== 'undefined'
? localStorage.getItem(key)
: null;
const value = stored ? JSON.parse(stored) : initial;
const store = writable<T>(value);
// Subscribe and persist every change
store.subscribe(currentValue => {
if (typeof localStorage !== 'undefined') {
localStorage.setItem(key, JSON.stringify(currentValue));
}
});
return store;
}
// Usage
export const theme = persistentWritable('theme', 'light');
export const userPrefs = persistentWritable('prefs', { language: 'en', notifications: true });<script>
import { theme } from './persistent.store';
</script>
<!-- Value persists across reloads -->
<select bind:value={$theme}>
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>Note:
localStorageis not available during server-side rendering (SvelteKit SSR). Always guard withtypeof localStorage !== 'undefined'or useonMountfor localStorage access.
Still Not Working?
Verify the store is exported and imported correctly. If you import from two different module paths (relative vs absolute), you get two separate store instances:
// Two different import paths = two separate instances
import { count } from './store'; // Instance A
import { count } from '../lib/store'; // Instance B — different object!
// Changes to instance A don't affect instance BUse Svelte DevTools (browser extension) to inspect store values in real time. The extension shows all writable stores, their current values, and a history of changes. If the store updates but the template doesn’t, the issue is in the component; if the store doesn’t update, the issue is in the store logic.
Check for SvelteKit SSR issues. Stores initialized at module level are shared across all server-side requests unless you use context-based stores:
// WRONG for SSR — module-level store shared across requests
export const user = writable(null);
// CORRECT for SSR — use SvelteKit's $page.data or context API
// Pass data through load functions and page.dataCheck for reactive statement issues in Svelte 5 (Runes). Svelte 5 introduces $state, $derived, and $effect as replacements for the store pattern. If you’re on Svelte 5, the store API still works but the new runes are the preferred approach:
<!-- Svelte 5 runes (alternative to stores for local state) -->
<script>
let count = $state(0);
let doubled = $derived(count * 2);
$effect(() => {
console.log('count changed:', count);
});
</script>
<p>{count} × 2 = {doubled}</p>
<button onclick={() => count++}>+</button>For related Svelte issues, see Fix: Vue Reactive Data Not Updating and Fix: React useState Not Updating.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Paraglide Not Working — Messages Not Loading, Compiler Errors, or Framework Integration Issues
How to fix Paraglide.js i18n issues — message compilation, type-safe translations, SvelteKit and Next.js integration, language switching, and message extraction from existing code.
Fix: Svelte 5 Runes Not Working — $state Not Reactive, $derived Not Updating, or $effect Running Twice
How to fix Svelte 5 Runes issues — $state and $state.raw reactivity, $derived computations, $effect lifecycle, $props and $bindable, migration from Svelte 4 stores, and component patterns.
Fix: CodeMirror Not Working — Editor Not Rendering, Extensions Not Loading, or React State Out of Sync
How to fix CodeMirror 6 issues — basic setup, language and theme extensions, React integration, vim mode, collaborative editing, custom keybindings, and read-only mode.
Fix: GSAP Not Working — Animations Not Playing, ScrollTrigger Not Firing, or React Cleanup Issues
How to fix GSAP animation issues — timeline and tween basics, ScrollTrigger setup, React useGSAP hook, cleanup and context, SplitText, stagger animations, and Next.js integration.