Skip to main content
Advertisement

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, use subscribe() 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 CaseRecommended Approach
Component-internal state$state() Rune
State shared between componentsSvelte stores or $state in .svelte.js
Third-party library integrationSvelte stores (contract-compatible)
SSR environmentsStores (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 TypeFunctionUse Case
writablewritable(value)Read/write state
readablereadable(value, start)External data sources (timers, etc.)
derivedderived(stores, fn)Values computed from other stores
Custom storeObject with subscribeBusiness logic encapsulation
$ auto-subscribe$store in componentsEliminate boilerplate

The next chapter covers SvelteKit routing, data loading, layouts, and adapters.

Advertisement