iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Stores

Stores are Svelte’s answer to cross-component state. The store interface is a single method — subscribe(callback) — plus optional set and update. Inside any component, prefix the store with $ for auto-subscription and tidy templates.

writable, readable, derived, custom

EXAMPLE
<script>
import { writable, readable, derived, get } from 'svelte/store';

// 1) writable — basic mutable store
export const count = writable(0);

// In any component:
//   <script>
//     import { count } from './stores.js';
//   </script>
//   <p>Count: {$count}</p>     ← the $ prefix subscribes for you
//   <button on:click={() => $count++}>+1</button>

// Methods
count.set(5);
count.update((n) => n + 1);
const current = get(count);             // read once, no subscription

// 2) readable — store that produces values you can't write to
export const now = readable(new Date(), (set) => {
    const id = setInterval(() => set(new Date()), 1000);
    return () => clearInterval(id);       // teardown when last subscriber leaves
});

// 3) derived — store computed from one or more other stores
import { user, cart } from './stores.js';
export const totalCents = derived(cart, ($cart) =>
    $cart.reduce((sum, line) => sum + line.priceCents * line.qty, 0),
);

export const summary = derived([user, totalCents], ([$user, $total]) => ({
    hello: $user ? `Hi ${$user.name}` : 'Guest',
    pretty: `$${($total / 100).toFixed(2)}`,
}));

// Async derived — emit when the promise resolves
export const profile = derived(userId, ($id, set) => {
    if (!$id) return set(null);
    fetch(`/api/users/${$id}`).then((r) => r.json()).then(set);
}, null);

// 4) Custom store — encapsulate state + API
function createCount() {
    const { subscribe, set, update } = writable(0);
    return {
        subscribe,
        increment: () => update((n) => n + 1),
        decrement: () => update((n) => n - 1),
        reset:     () => set(0),
    };
}
export const counter = createCount();

// In a component:
//   import { counter } from './stores.js';
//   <button on:click={counter.increment}>+1</button>
//   <button on:click={counter.reset}>reset</button>
//   <p>{$counter}</p>

// 5) Persisted store — localStorage round-trip
function persisted(key, initial) {
    const stored = typeof localStorage !== 'undefined' ? localStorage.getItem(key) : null;
    const store  = writable(stored ? JSON.parse(stored) : initial);
    store.subscribe((v) => {
        if (typeof localStorage !== 'undefined') localStorage.setItem(key, JSON.stringify(v));
    });
    return store;
}
export const theme = persisted('theme', 'light');

// 6) Two-way binding to a store
//   <input bind:value={$theme} />
// Writing to the input now writes the store; the store change re-renders consumers.

// 7) Async loading store (loading / data / error)
function asyncStore(fetcher) {
    const { subscribe, set } = writable({ loading: true, data: null, error: null });
    fetcher()
        .then((data) => set({ loading: false, data, error: null }))
        .catch((error) => set({ loading: false, data: null, error }));
    return { subscribe };
}
export const todos = asyncStore(() => fetch('/api/todos').then((r) => r.json()));

// Component
//   {#if $todos.loading}
//       <p>Loading…</p>
//   {:else if $todos.error}
//       <p>Error: {$todos.error.message}</p>
//   {:else}
//       <ul>{#each $todos.data as t (t.id)}<li>{t.text}</li>{/each}</ul>
//   {/if}

// 8) Context vs stores
// Context: passes any value down a component tree without prop-drilling.
// Stores:  values change over time and consumers want auto-updates.
// Combine them — put a store in context for per-tree state (e.g. a modal stack).

import { setContext, getContext } from 'svelte';
import { writable } from 'svelte/store';

// In parent
const stack = writable([]);
setContext('modal-stack', stack);

// In any descendant
const stack = getContext('modal-stack');
stack.update((s) => [...s, { id, content }]);

// 9) Stores in SvelteKit — beware of cross-request leakage
// On the server, a module-scope writable is shared across ALL requests.
// Use a per-request store (load() or +layout.svelte) for user-specific state
// OR Svelte 5 runes + a Context provider for clean isolation.

// 10) Common bugs
//   • Importing a store and reading it as $store outside a .svelte file (only in components)
//   • Mutating an object inside a writable WITHOUT calling .set / .update
//        $cart.push(item)   ← won't trigger subscribers
//        cart.update((c) => [...c, item])   ← does
//   • Subscribing manually and forgetting to unsubscribe → memory leak
//        Use $store in the template OR an onDestroy() cleanup
//   • Using get(store) to drive UI logic — bypasses reactivity
//   • Module-scope writable on SvelteKit server → cross-request bleed
</script>

Why it matters

The $ prefix auto-subscribes and unsubscribes — let it. Reach for a custom store when you want to expose a small API surface (increment, reset) instead of leaking raw set everywhere, and remember to set or update after mutating — in-place mutation of a stored object never notifies subscribers.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
// Old API still works in Svelte 5 alongside runes.
import { writable } from 'svelte/store';
export const count = writable(0);
Try it Yourself »

Exercise

Auto-subscribe to a store in markup.

<p>Count: { count}</p>

Test yourself

Q1. A writable store comes from…
Q2. You auto-subscribe to a store in markup with…
Q3. In Svelte 5, plain reactive state often replaces…

Discussion

Loading…