17.5 Stores
Svelte Stores are a mechanism for managing state outside the component tree. They are used for state that needs to be shared across multiple components — user authentication info, theme settings, shopping cart data, and similar global state.
The Svelte Store Contract
A Svelte store is an object that follows a simple contract:
interface Store<T> {
subscribe(callback: (value: T) => void): () => void;
// set and update only exist on writable stores
}
subscribe(): Calls the callback whenever the value changes. Returns an unsubscribe function.set(): Sets a new value (writable only)update(): Updates the value using a function that receives the previous value (writable only)
writable() — Writable Store
// src/lib/stores/count.js
import { writable } from 'svelte/store';
// Create a writable store with initial value 0
export const count = writable(0);
<!-- Counter.svelte -->
<script>
import { count } from '$lib/stores/count.js';
function increment() {
count.update(n => n + 1);
}
function decrement() {
count.update(n => n - 1);
}
function reset() {
count.set(0);
}
</script>
<!-- $ prefix auto-subscribes (only in components) -->
<p>Current value: {$count}</p>
<button onclick={increment}>+1</button>
<button onclick={decrement}>-1</button>
<button onclick={reset}>Reset</button>
Manual Subscription (Outside Components)
import { count } from '$lib/stores/count.js';
// Manual subscription
const unsubscribe = count.subscribe(value => {
console.log('count changed:', value);
});
// Unsubscribe to prevent memory leaks
unsubscribe();
readable() — Read-Only Store
A store whose value cannot be changed externally. Used for timers, geolocation, browser events, and similar external data sources.
// src/lib/stores/time.js
import { readable } from 'svelte/store';
// Current time store (updates every second)
export const currentTime = readable(new Date(), (set) => {
const interval = setInterval(() => {
set(new Date());
}, 1000);
// Cleanup function (called when there are no more subscribers)
return () => clearInterval(interval);
});
<!-- Clock.svelte -->
<script>
import { currentTime } from '$lib/stores/time.js';
let timeString = $derived(
$currentTime.toLocaleTimeString('en-US', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
})
);
</script>
<p>Current time: {timeString}</p>
Mouse Position Readable Store
// src/lib/stores/mouse.js
import { readable } from 'svelte/store';
export const mousePosition = readable({ x: 0, y: 0 }, (set) => {
function handleMouseMove(event) {
set({ x: event.clientX, y: event.clientY });
}
window.addEventListener('mousemove', handleMouseMove);
return () => {
window.removeEventListener('mousemove', handleMouseMove);
};
});
derived() — Derived Store
A store whose value is computed from one or more other stores.
// src/lib/stores/cart.js
import { writable, derived } from 'svelte/store';
export const cartItems = writable([]);
// Derived from a single store
export const cartCount = derived(
cartItems,
($items) => $items.length
);
export const cartTotal = derived(
cartItems,
($items) => $items.reduce((sum, item) => sum + item.price * item.quantity, 0)
);
// Derived from multiple stores
import { writable } from 'svelte/store';
export const discountRate = writable(0);
export const finalTotal = derived(
[cartTotal, discountRate],
([$total, $discount]) => $total * (1 - $discount)
);
$ Auto-Subscribe Syntax
Prefixing a store with $ inside a component automatically subscribes to it and unsubscribes when the component is destroyed.
<script>
import { count } from '$lib/stores/count.js';
import { cartItems, cartTotal, finalTotal } from '$lib/stores/cart.js';
</script>
<!-- $ prefix = auto-subscribe -->
<p>Count: {$count}</p>
<p>Cart: {$cartItems.length} items</p>
<p>Subtotal: ${$cartTotal}</p>
<p>Total: ${$finalTotal}</p>
<!-- Also works in two-way bindings -->
<input bind:value={$count} type="number" />
Note: The
$auto-subscribe syntax can only be used in the top-level script of a component. Inside functions or module scripts, usesubscribe()manually.
Custom Store Pattern
You can encapsulate business logic directly in a store.
// src/lib/stores/counter.js
import { writable } from 'svelte/store';
function createCounter(initialValue = 0) {
const { subscribe, set, update } = writable(initialValue);
return {
subscribe, // Required: enables $counter syntax externally
increment: () => update(n => n + 1),
decrement: () => update(n => n - 1),
reset: () => set(initialValue),
setTo: (value) => set(value),
double: () => update(n => n * 2),
};
}
export const counter = createCounter(10);
<!-- Using in a component -->
<script>
import { counter } from '$lib/stores/counter.js';
</script>
<p>Value: {$counter}</p>
<button onclick={counter.increment}>+1</button>
<button onclick={counter.decrement}>-1</button>
<button onclick={counter.double}>×2</button>
<button onclick={counter.reset}>Reset</button>
Practical Example 1: Theme Store
// src/lib/stores/theme.svelte.js
import { writable } from 'svelte/store';
import { browser } from '$app/environment';
function createThemeStore() {
const saved = browser ? localStorage.getItem('theme') : null;
const initial = saved ?? 'system';
const { subscribe, set } = writable(initial);
function applyTheme(theme) {
if (!browser) return;
let resolved = theme;
if (theme === 'system') {
resolved = window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light';
}
document.documentElement.setAttribute('data-theme', resolved);
localStorage.setItem('theme', theme);
set(theme);
}
return {
subscribe,
setTheme: applyTheme,
toggle: () => {
let current;
subscribe(v => current = v)();
applyTheme(current === 'dark' ? 'light' : 'dark');
},
};
}
export const theme = createThemeStore();
<!-- ThemeToggle.svelte -->
<script>
import { theme } from '$lib/stores/theme.svelte.js';
</script>
<div class="theme-switcher">
{#each ['light', 'dark', 'system'] as t}
<button
class:active={$theme === t}
onclick={() => theme.setTheme(t)}
>
{#if t === 'light'}☀️{:else if t === 'dark'}🌙{:else}🖥️{/if}
{t}
</button>
{/each}
</div>
Practical Example 2: User Authentication Store
// src/lib/stores/auth.js
import { writable, derived } from 'svelte/store';
const createAuthStore = () => {
const user = writable(null);
const loading = writable(false);
const error = writable(null);
const isAuthenticated = derived(user, $user => !!$user);
const isAdmin = derived(user, $user => $user?.role === 'admin');
async function login(credentials) {
loading.set(true);
error.set(null);
try {
const res = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(credentials),
});
if (!res.ok) throw new Error('Login failed');
const userData = await res.json();
user.set(userData);
return { success: true };
} catch (err) {
error.set(err.message);
return { success: false, error: err.message };
} finally {
loading.set(false);
}
}
async function logout() {
await fetch('/api/auth/logout', { method: 'POST' });
user.set(null);
error.set(null);
}
async function checkSession() {
loading.set(true);
try {
const res = await fetch('/api/auth/me');
if (res.ok) {
const userData = await res.json();
user.set(userData);
}
} catch {
user.set(null);
} finally {
loading.set(false);
}
}
return {
user: { subscribe: user.subscribe },
loading: { subscribe: loading.subscribe },
error: { subscribe: error.subscribe },
isAuthenticated,
isAdmin,
login,
logout,
checkSession,
};
};
export const auth = createAuthStore();
<!-- NavBar.svelte -->
<script>
import { auth } from '$lib/stores/auth.js';
</script>
<nav>
<a href="/">Home</a>
{#if $auth.isAuthenticated}
<a href="/dashboard">Dashboard</a>
{#if $auth.isAdmin}
<a href="/admin">Admin</a>
{/if}
<span>{$auth.user?.name}</span>
<button onclick={auth.logout}>Logout</button>
{:else}
<a href="/login">Login</a>
{/if}
</nav>
Stores vs Runes in Svelte 5
In Svelte 5, Runes can replace many store use cases.
| Use Case | Recommended Approach |
|---|---|
| Component-internal state | $state() Rune |
| State shared between components | Svelte stores or $state in .svelte.js |
| Third-party library integration | Svelte stores (contract-compatible) |
| SSR environments | Stores (SvelteKit server/client separation) |
// Global state with Runes (src/lib/globalState.svelte.js)
let count = $state(0);
export function getCount() { return count; }
export function setCount(v) { count = v; }
export function increment() { count++; }
<!-- Any component -->
<script>
import { getCount, increment } from '$lib/globalState.svelte.js';
</script>
<p>Global count: {getCount()}</p>
<button onclick={increment}>+1</button>
Global State Management Pattern Comparison
// Pattern 1: Simple writable store
import { writable } from 'svelte/store';
export const items = writable([]);
// Pattern 2: Custom store (encapsulated business logic)
function createItemsStore() {
const { subscribe, update } = writable([]);
return {
subscribe,
add: (item) => update(items => [...items, item]),
remove: (id) => update(items => items.filter(i => i.id !== id)),
};
}
export const items = createItemsStore();
// Pattern 3: Runes-based (Svelte 5, .svelte.js file)
let _items = $state([]);
export const items = {
get value() { return _items; },
add(item) { _items.push(item); },
remove(id) { _items = _items.filter(i => i.id !== id); },
};
Pro Tips
Tip 1: Lazy-Load Store Initial Values
import { writable } from 'svelte/store';
import { browser } from '$app/environment';
// Only access localStorage in browser
const storedValue = browser ? JSON.parse(localStorage.getItem('settings') ?? 'null') : null;
export const settings = writable(storedValue ?? {
theme: 'light',
language: 'en',
notifications: true,
});
// Auto-save on change
if (browser) {
settings.subscribe(value => {
localStorage.setItem('settings', JSON.stringify(value));
});
}
Tip 2: Type-Safe Stores (TypeScript)
// src/lib/stores/typed.ts
import { writable, derived, type Readable } from 'svelte/store';
interface User {
id: number;
name: string;
email: string;
role: 'user' | 'admin';
}
export const user = writable<User | null>(null);
export const isAdmin: Readable<boolean> = derived(
user,
$user => $user?.role === 'admin' ?? false
);
Tip 3: Combining Stores
import { derived } from 'svelte/store';
import { cartItems } from './cart.js';
import { user } from './auth.js';
// Derived store combining multiple stores
export const checkoutData = derived(
[cartItems, user],
([$items, $user]) => ({
items: $items,
userId: $user?.id,
canCheckout: $items.length > 0 && !!$user,
total: $items.reduce((s, i) => s + i.price * i.quantity, 0),
})
);
Summary
| Store Type | Function | Use Case |
|---|---|---|
writable | writable(value) | Read/write state |
readable | readable(value, start) | External data sources (timers, etc.) |
derived | derived(stores, fn) | Values computed from other stores |
| Custom store | Object with subscribe | Business logic encapsulation |
$ auto-subscribe | $store in components | Eliminate boilerplate |
The next chapter covers SvelteKit routing, data loading, layouts, and adapters.