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>
A single character.
Discussion
Loading…