Skip to content

Fix: Svelte Store Not Updating — Reactive Store Issues

FixDevs ·

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 0

Or a derived store doesn’t update when its source changes:

const doubled = derived(count, $count => $count * 2);
// doubled stays stale after count changes

Or 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() or update() is called. Inside update(), 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 $store auto-subscription syntax (e.g., $count) is compiled into a subscription/unsubscription. Using store without the $ gives you the store object, not its current value.
  • Manual subscription without unsubscribing — calling store.subscribe() without returning the unsubscribe function in onDestroy causes 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 notified

Same 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() or Array.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: localStorage is not available during server-side rendering (SvelteKit SSR). Always guard with typeof localStorage !== 'undefined' or use onMount for 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 B

Use 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.data

Check 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.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles