The Svelte store system provides a simple and powerful way to manage reactive state outside of components. Stores are observable values that can be shared across your application, allowing multiple components to reactively respond to state changes.
Stores in Svelte follow a contract based on subscriptions. Any object that implements the subscribe method is considered a store. The store system includes built-in functions for creating writable, readable, and derived stores, as well as utilities for converting between stores and runes.
Callback function invoked when a store's value changes.
type Subscriber<T> = (value: T) => void;Parameters:
value: The new value of the storeDescription: This callback is invoked immediately upon subscription with the current value, and subsequently whenever the store's value changes.
Function returned from subscribe() to cancel the subscription.
type Unsubscriber = () => void;Description: Calling this function will stop the subscriber from receiving updates and clean up any resources associated with the subscription.
Function that receives the current store value and returns a new value.
type Updater<T> = (value: T) => T;Parameters:
value: The current value of the storeReturns: The new value for the store
Description:
Used with the update() method of writable stores to transform the current value based on its previous state.
Lifecycle callback invoked when the first subscriber subscribes and optionally when the last subscriber unsubscribes.
type StartStopNotifier<T> = (
set: (value: T) => void,
update: (fn: Updater<T>) => void
) => void | (() => void);Parameters:
set: Function to set the store's value directlyupdate: Function to update the store's value via an updater functionReturns: Optionally, a cleanup function that is called when the last remaining subscriber unsubscribes
Description: This function is called when transitioning from zero to one subscribers. It's useful for establishing subscriptions to external data sources, starting intervals, or other initialization logic. The optional cleanup function is called when transitioning from one to zero subscribers.
Example:
const store = writable(0, (set) => {
console.log('First subscriber');
const interval = setInterval(() => {
set(Math.random());
}, 1000);
return () => {
console.log('Last subscriber unsubscribed');
clearInterval(interval);
};
});Interface for read-only stores that can be subscribed to.
interface Readable<T> {
/**
* Subscribe on value changes.
* @param run subscription callback
* @param invalidate cleanup callback
*/
subscribe(this: void, run: Subscriber<T>, invalidate?: () => void): Unsubscriber;
}Methods:
Subscribes to store value changes.
Parameters:
run: Callback invoked immediately with the current value and on each subsequent changeinvalidate (optional): Callback invoked just before run is called, useful for cleanupReturns: Unsubscriber function to cancel the subscription
Example:
const unsubscribe = store.subscribe(value => {
console.log('Store value:', value);
});
// Later, when done
unsubscribe();Interface for stores that can be both read from and written to.
interface Writable<T> extends Readable<T> {
/**
* Set value and inform subscribers.
* @param value to set
*/
set(this: void, value: T): void;
/**
* Update value using callback and inform subscribers.
* @param updater callback
*/
update(this: void, updater: Updater<T>): void;
}Methods:
Replaces the store's value and notifies all subscribers.
Parameters:
value: The new value to setExample:
count.set(42);Updates the store's value based on its current value.
Parameters:
updater: Function that receives the current value and returns the new valueExample:
count.update(n => n + 1);Creates a writable store with subscribe, set, and update methods.
function writable<T>(
value?: T | undefined,
start?: StartStopNotifier<T> | undefined
): Writable<T>;Parameters:
value (optional): Initial value of the storestart (optional): Function called when the first subscriber subscribesReturns:
A Writable<T> store object
Description:
Creates a store that can be updated from outside components. The store starts with the given initial value and notifies all subscribers whenever set() or update() is called.
Example:
import { writable } from 'svelte/store';
// Simple counter store
const count = writable(0);
// Store with start/stop logic
const time = writable(new Date(), (set) => {
const interval = setInterval(() => {
set(new Date());
}, 1000);
return () => clearInterval(interval);
});Usage in Components:
<script>
import { count } from './stores.js';
function increment() {
count.update(n => n + 1);
}
function reset() {
count.set(0);
}
</script>
<h1>The count is {$count}</h1>
<button on:click={increment}>+1</button>
<button on:click={reset}>reset</button>Creates a read-only store that cannot be modified from outside.
function readable<T>(
value?: T | undefined,
start?: StartStopNotifier<T> | undefined
): Readable<T>;Parameters:
value (optional): Initial value of the storestart (optional): Function called when the first subscriber subscribes. This is where you can set up logic to update the store's value.Returns:
A Readable<T> store object
Description:
Creates a read-only store. Unlike writable stores, readable stores can only be updated from within the start function, making them ideal for representing values from external sources.
Example:
import { readable } from 'svelte/store';
// Mouse position store
const mousePosition = readable({ x: 0, y: 0 }, (set) => {
function handleMouseMove(event) {
set({ x: event.clientX, y: event.clientY });
}
document.addEventListener('mousemove', handleMouseMove);
return () => {
document.removeEventListener('mousemove', handleMouseMove);
};
});
// WebSocket store
const liveData = readable(null, (set) => {
const ws = new WebSocket('wss://example.com/data');
ws.addEventListener('message', (event) => {
set(JSON.parse(event.data));
});
return () => ws.close();
});Creates a store whose value is computed from one or more other stores.
// Synchronous derivation
function derived<S extends Stores, T>(
stores: S,
fn: (values: StoresValues<S>) => T,
initial_value?: T | undefined
): Readable<T>;
// Asynchronous derivation with set/update
function derived<S extends Stores, T>(
stores: S,
fn: (
values: StoresValues<S>,
set: (value: T) => void,
update: (fn: Updater<T>) => void
) => Unsubscriber | void,
initial_value?: T | undefined
): Readable<T>;Parameters:
stores: A single store or array of stores to derive fromfn: Function that computes the derived value
set, and update functionsinitial_value (optional): Initial value before the first derivation completesReturns:
A Readable<T> store with the derived value
Description:
Creates a store that automatically updates when any of its source stores change. The function can be synchronous (returning a value) or asynchronous (calling set to update the value, and optionally returning a cleanup function).
Example - Synchronous:
import { derived } from 'svelte/store';
import { firstName, lastName } from './stores.js';
// Single store derivation
const doubled = derived(count, $count => $count * 2);
// Multiple stores derivation
const fullName = derived(
[firstName, lastName],
([$firstName, $lastName]) => `${$firstName} ${$lastName}`
);
// With initial value
const delayed = derived(
input,
$input => $input.toUpperCase(),
'loading...' // shown until first derivation
);Example - Asynchronous:
// Async derivation with cleanup
const searchResults = derived(
searchQuery,
($query, set) => {
if (!$query) {
set([]);
return;
}
const controller = new AbortController();
fetch(`/api/search?q=${$query}`, {
signal: controller.signal
})
.then(r => r.json())
.then(data => set(data))
.catch(err => {
if (err.name !== 'AbortError') {
set([]);
}
});
// Cleanup function - cancels fetch if query changes
return () => controller.abort();
},
[] // initial value
);Wraps a store to make it read-only.
function readonly<T>(store: Readable<T>): Readable<T>;Parameters:
store: A readable store to make read-onlyReturns: A read-only view of the store
Description:
Takes any readable store and returns a new store that only exposes the subscribe method. This is useful when you want to expose a store publicly but prevent external code from modifying it.
Example:
import { writable, readonly } from 'svelte/store';
// Internal writable store
const _count = writable(0);
// Public read-only interface
export const count = readonly(_count);
// Export functions to modify the store
export function increment() {
_count.update(n => n + 1);
}
export function reset() {
_count.set(0);
}Usage:
<script>
import { count, increment, reset } from './stores.js';
// Can subscribe to count
console.log($count);
// Cannot modify directly
// count.set(10); // Error: Property 'set' does not exist
// Must use exported functions
increment();
</script>Synchronously retrieves the current value from a store.
function get<T>(store: Readable<T>): T;Parameters:
store: A readable storeReturns: The current value of the store
Description:
Subscribes to a store, gets its current value, and immediately unsubscribes. This should be used sparingly as it creates a non-reactive one-time read. Prefer using $store syntax in components or subscribing for reactive updates.
Example:
import { get } from 'svelte/store';
import { count } from './stores.js';
// Get current value without subscribing
const currentCount = get(count);
console.log(currentCount);
// Use in event handlers
function handleClick() {
const value = get(count);
if (value > 10) {
alert('Count is high!');
}
}
// Use in derived stores when you need non-reactive read
const derived = readable(null, (set) => {
const interval = setInterval(() => {
// Non-reactive read of another store
const currentValue = get(someStore);
set(currentValue * 2);
}, 1000);
return () => clearInterval(interval);
});Warning:
Use get() carefully. In most cases, you should use reactive subscriptions ($store or .subscribe()) instead. Use get() only when you need a one-time snapshot of the value, such as in event handlers or imperative code.
Converts reactive state (runes) into a store.
// Writable store
function toStore<V>(get: () => V, set: (v: V) => void): Writable<V>;
// Readable store
function toStore<V>(get: () => V): Readable<V>;Parameters:
get: Function that returns the current valueset (optional): Function to update the value. If provided, creates a writable store; otherwise, creates a readable store.Returns:
A Writable<V> or Readable<V> store depending on whether set is provided
Description: Bridges Svelte 5's runes system with the classic store API. This allows you to expose rune-based state as stores for compatibility with libraries or components expecting stores.
Example:
<script>
import { toStore } from 'svelte/store';
// Rune-based state
let count = $state(0);
// Convert to writable store
const countStore = toStore(
() => count,
(v) => count = v
);
// Now can be used as a regular store
countStore.subscribe(value => console.log(value));
countStore.set(5);
countStore.update(n => n + 1);
</script>Example - Readable:
<script>
import { toStore } from 'svelte/store';
let x = $state(0);
let y = $state(0);
// Convert derived state to readable store
const position = toStore(() => ({ x, y }));
// Can be subscribed to, but not modified
position.subscribe(pos => console.log(pos));
</script>
<svelte:window
on:mousemove={(e) => {
x = e.clientX;
y = e.clientY;
}}
/>Example - Integration with Store-based Library:
<script>
import { toStore } from 'svelte/store';
import ThirdPartyComponent from 'some-library';
// Your rune-based state
let myValue = $state(0);
// Convert for compatibility
const myStore = toStore(
() => myValue,
(v) => myValue = v
);
</script>
<!-- Pass store to component expecting store API -->
<ThirdPartyComponent store={myStore} />Converts a store into a reactive object with a .current property.
// From writable store
function fromStore<V>(store: Writable<V>): {
current: V;
};
// From readable store
function fromStore<V>(store: Readable<V>): {
readonly current: V;
};Parameters:
store: A readable or writable storeReturns:
An object with a current property that reflects the store's value. If the source is writable, current is settable; otherwise, it's readonly.
Description:
Bridges the classic store API with Svelte 5's runes system. The returned object's current property is reactive and can be used with runes like $derived and $effect.
Example:
<script>
import { fromStore } from 'svelte/store';
import { writable } from 'svelte/store';
// Existing store (maybe from a library)
const countStore = writable(0);
// Convert to rune-compatible object
const count = fromStore(countStore);
// Use with runes
const doubled = $derived(count.current * 2);
$effect(() => {
console.log('Count changed to:', count.current);
});
// Can read and write through .current
function increment() {
count.current += 1;
}
</script>
<h1>{count.current}</h1>
<p>Doubled: {doubled}</p>
<button on:click={increment}>Increment</button>Example - Readonly:
<script>
import { fromStore } from 'svelte/store';
import { readable } from 'svelte/store';
const timeStore = readable(new Date(), (set) => {
const interval = setInterval(() => set(new Date()), 1000);
return () => clearInterval(interval);
});
// Convert to rune-compatible readonly object
const time = fromStore(timeStore);
// Can read but not write
const hours = $derived(time.current.getHours());
const minutes = $derived(time.current.getMinutes());
// time.current = new Date(); // TypeScript error: readonly property
</script>
<p>Time: {hours}:{minutes}</p>Example - Integration with Store-based State Management:
<script>
import { fromStore } from 'svelte/store';
import { userStore, settingsStore } from './legacy-stores.js';
// Convert stores to rune-compatible objects
const user = fromStore(userStore);
const settings = fromStore(settingsStore);
// Use with modern rune-based derivations
const greeting = $derived(
settings.current.language === 'es'
? `Hola, ${user.current.name}`
: `Hello, ${user.current.name}`
);
// Use in effects
$effect(() => {
if (user.current.isAuthenticated) {
loadUserData(user.current.id);
}
});
</script>
<h1>{greeting}</h1>You can create custom stores by implementing the store contract (subscribe method) and adding custom methods:
import { writable } from 'svelte/store';
function createCounter(initial = 0) {
const { subscribe, set, update } = writable(initial);
return {
subscribe,
increment: () => update(n => n + 1),
decrement: () => update(n => n - 1),
reset: () => set(initial)
};
}
export const count = createCounter(0);Usage:
<script>
import { count } from './stores.js';
</script>
<h1>{$count}</h1>
<button on:click={count.increment}>+</button>
<button on:click={count.decrement}>-</button>
<button on:click={count.reset}>Reset</button>import { writable, derived } from 'svelte/store';
function debounced(store, delay = 500) {
return derived(store, ($value, set) => {
const timeout = setTimeout(() => set($value), delay);
return () => clearTimeout(timeout);
});
}
const input = writable('');
const debouncedInput = debounced(input);import { writable } from 'svelte/store';
function persistent(key, initial) {
const stored = localStorage.getItem(key);
const value = stored ? JSON.parse(stored) : initial;
const store = writable(value);
store.subscribe(value => {
localStorage.setItem(key, JSON.stringify(value));
});
return store;
}
export const preferences = persistent('preferences', {
theme: 'dark',
language: 'en'
});import { derived, writable } from 'svelte/store';
const firstName = writable('John');
const lastName = writable('Doe');
const age = writable(30);
// Compose multiple stores
const person = derived(
[firstName, lastName, age],
([$firstName, $lastName, $age]) => ({
fullName: `${$firstName} ${$lastName}`,
age: $age,
isAdult: $age >= 18
})
);import { writable, derived } from 'svelte/store';
function asyncStore(fetcher) {
const loading = writable(false);
const error = writable(null);
const data = writable(null);
async function load(...args) {
loading.set(true);
error.set(null);
try {
const result = await fetcher(...args);
data.set(result);
} catch (e) {
error.set(e);
} finally {
loading.set(false);
}
}
return {
loading: { subscribe: loading.subscribe },
error: { subscribe: error.subscribe },
data: { subscribe: data.subscribe },
load
};
}
// Usage
const users = asyncStore((id) =>
fetch(`/api/users/${id}`).then(r => r.json())
);
// In component
users.load(123);
console.log($users.loading); // true
console.log($users.data); // user data when loadedimport { derived } from 'svelte/store';
const cart = writable([
{ id: 1, name: 'Item 1', price: 10, quantity: 2 },
{ id: 2, name: 'Item 2', price: 20, quantity: 1 }
]);
const discountCode = writable('SAVE10');
const cartSummary = derived(
[cart, discountCode],
([$cart, $discountCode]) => {
const subtotal = $cart.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
const discount = $discountCode === 'SAVE10'
? subtotal * 0.1
: 0;
const total = subtotal - discount;
return {
items: $cart.length,
subtotal,
discount,
total
};
}
);import { writable } from 'svelte/store';
function createStoreWithMiddleware(initial, middleware = []) {
const { subscribe, set, update } = writable(initial);
function setWithMiddleware(value) {
const processed = middleware.reduce(
(val, fn) => fn(val),
value
);
set(processed);
}
function updateWithMiddleware(fn) {
update(current => {
const newValue = fn(current);
return middleware.reduce((val, mw) => mw(val), newValue);
});
}
return {
subscribe,
set: setWithMiddleware,
update: updateWithMiddleware
};
}
// Example: validation middleware
const validatePositive = (value) => Math.max(0, value);
const roundToInt = (value) => Math.round(value);
const count = createStoreWithMiddleware(0, [
validatePositive,
roundToInt
]);
count.set(-5); // Actually sets to 0
count.set(3.7); // Actually sets to 4When migrating components to use stores instead of runes:
Before (Runes):
<script>
let count = $state(0);
let doubled = $derived(count * 2);
function increment() {
count += 1;
}
</script>
<h1>{count}</h1>
<p>Doubled: {doubled}</p>
<button on:click={increment}>+1</button>After (Stores):
<script>
import { writable, derived } from 'svelte/store';
const count = writable(0);
const doubled = derived(count, $count => $count * 2);
function increment() {
count.update(n => n + 1);
}
</script>
<h1>{$count}</h1>
<p>Doubled: {$doubled}</p>
<button on:click={increment}>+1</button>When modernizing to use runes:
Before (Stores):
<script>
import { writable, derived } from 'svelte/store';
const count = writable(0);
const doubled = derived(count, $count => $count * 2);
</script>After (Runes):
<script>
let count = $state(0);
let doubled = $derived(count * 2);
</script>With Interoperability (Hybrid Approach):
<script>
import { toStore, fromStore } from 'svelte/store';
import { legacyStore } from './old-stores.js';
// Convert legacy store to rune
const legacyValue = fromStore(legacyStore);
// Modern rune-based state
let modernValue = $state(0);
// Expose modern state as store for compatibility
const modernStore = toStore(
() => modernValue,
(v) => modernValue = v
);
// Use both seamlessly
let combined = $derived(legacyValue.current + modernValue);
</script>// Bad
const s = writable(0);
// Good
const userCount = writable(0);
const isAuthenticated = writable(false);
const activeUsers = writable([]);// Bad - too much logic in store
const user = writable({
name: '',
validateAndUpdateName(newName) {
if (newName.length > 3) {
this.name = newName;
this.save();
}
}
});
// Good - separate concerns
const userName = writable('');
function updateUserName(newName) {
if (newName.length > 3) {
userName.set(newName);
saveUserName(newName);
}
}// Bad - manual synchronization
const firstName = writable('John');
const lastName = writable('Doe');
const fullName = writable('John Doe');
firstName.subscribe($firstName => {
fullName.set(`${$firstName} ${get(lastName)}`);
});
// Good - automatic synchronization
const firstName = writable('John');
const lastName = writable('Doe');
const fullName = derived(
[firstName, lastName],
([$firstName, $lastName]) => `${$firstName} ${$lastName}`
);// Bad - memory leak
onMount(() => {
someStore.subscribe(value => {
console.log(value);
});
});
// Good - cleanup
onMount(() => {
const unsubscribe = someStore.subscribe(value => {
console.log(value);
});
return unsubscribe;
});
// Best - use $store syntax in components (auto-cleanup)
// Just use {$someStore} in template or $someStore in script// store.js
import { writable, readonly } from 'svelte/store';
const _settings = writable({ theme: 'dark' });
export const settings = readonly(_settings);
export function updateTheme(theme) {
_settings.update(s => ({ ...s, theme }));
}Use stores when:
Use runes ($state) when:
// Provide sensible defaults
const userPreferences = writable({
theme: 'light',
language: 'en',
notifications: true
});
// Use derived for default fallbacks
const displayName = derived(
[userName, userEmail],
([$name, $email]) => $name || $email || 'Anonymous'
);// Bad - imperatively reading store often
function handleClick() {
const current = get(count);
console.log(current);
const other = get(otherStore);
console.log(other);
}
// Good - subscribe once if needed
const unsubscribe = count.subscribe(value => {
// React to changes
});
// Better - use $store syntax in components
// $: console.log($count, $otherStore);Derived stores only recalculate when their dependencies change:
const expensiveComputation = derived(
sourceStore,
$source => {
// This only runs when sourceStore changes
return performExpensiveOperation($source);
}
);// Bad - subscribes multiple times
function Component() {
someStore.subscribe(a => { /* ... */ });
someStore.subscribe(b => { /* ... */ });
someStore.subscribe(c => { /* ... */ });
}
// Good - single subscription
function Component() {
someStore.subscribe(value => {
handleA(value);
handleB(value);
handleC(value);
});
}// Less efficient - triggers multiple updates
count.set(1);
count.set(2);
count.set(3);
// More efficient - single update
count.set(3);
// Or use update for transformations
count.update(n => n + 3);Use the start parameter for expensive operations:
// Only initialize when someone subscribes
const expensiveData = readable(null, (set) => {
// Expensive initialization
const data = loadLargeDataset();
set(data);
// Cleanup when no subscribers
return () => {
cleanupData(data);
};
});import { writable, derived, type Writable, type Readable } from 'svelte/store';
interface User {
id: number;
name: string;
email: string;
}
const user = writable<User>({
id: 1,
name: 'John',
email: 'john@example.com'
});
const userName: Readable<string> = derived(
user,
$user => $user.name
);interface CounterStore extends Writable<number> {
increment: () => void;
decrement: () => void;
reset: () => void;
}
function createCounter(initial = 0): CounterStore {
const { subscribe, set, update } = writable(initial);
return {
subscribe,
set,
update,
increment: () => update(n => n + 1),
decrement: () => update(n => n - 1),
reset: () => set(initial)
};
}function createArrayStore<T>(initial: T[] = []): Writable<T[]> & {
push: (item: T) => void;
pop: () => T | undefined;
clear: () => void;
} {
const { subscribe, set, update } = writable<T[]>(initial);
return {
subscribe,
set,
update,
push: (item: T) => update(arr => [...arr, item]),
pop: () => {
let popped: T | undefined;
update(arr => {
popped = arr[arr.length - 1];
return arr.slice(0, -1);
});
return popped;
},
clear: () => set([])
};
}
const numbers = createArrayStore<number>([1, 2, 3]);
const names = createArrayStore<string>(['Alice', 'Bob']);Version: Svelte 5.46.1
Module: svelte/store