Skip to main content
Technology & EngineeringSvelte249 lines

Stores

Svelte stores and state management patterns including writable, readable, derived, and custom stores

Quick Summary28 lines
You are an expert in Svelte stores and state management, helping developers manage shared state across components using built-in store primitives, custom stores, and modern Svelte 5 alternatives.

## Key Points

- **Use custom stores to encapsulate business logic.** Expose semantic methods (`addItem`, `toggle`) instead of raw `set`/`update`.
- **Prefer `derived` for computed values** rather than subscribing to one store and setting another.
- **Avoid store subscriptions in server-side code.** Stores are singletons, which means shared state across requests on the server. Use SvelteKit's `locals` or load function data instead.
- **Use runes for new Svelte 5 projects** when you do not need the store contract for interop. They offer a simpler API and deep reactivity.
- **Clean up manual subscriptions.** If you call `store.subscribe()` directly (outside a component's `$` shorthand), you must call the returned unsubscribe function to prevent memory leaks.
- **Forgetting the `$` prefix.** Referencing `count` instead of `$count` in a template gives you the store object, not its value.
- **Over-subscribing.** Each `$store` reference in a component creates a subscription. For expensive derived computations, extract the value once into a local variable.
- **Mutating store values in place.** `$store.push(item)` does not trigger an update. You must reassign: `$store = [...$store, item]` or use `store.update()`.
- **Using stores where props suffice.** If state is only needed by a parent and its direct children, pass it as props rather than creating a global store.

## Quick Example

```js
// src/lib/stores/counter.js
import { writable } from 'svelte/store';

export const count = writable(0);
```

```js
export const theme = persisted('theme', 'light');
```
skilldb get svelte-skills/StoresFull skill: 249 lines
Paste into your CLAUDE.md or agent config

Stores and State Management — Svelte

You are an expert in Svelte stores and state management, helping developers manage shared state across components using built-in store primitives, custom stores, and modern Svelte 5 alternatives.

Core Philosophy

Svelte stores solve the problem of sharing reactive state across components that are not in a direct parent-child relationship. The store contract is deliberately minimal: any object with a subscribe method that returns an unsubscribe function qualifies as a store. This simplicity means you can wrap virtually any external data source — WebSockets, event emitters, browser APIs — into something that works with Svelte's $ auto-subscription syntax.

The built-in store primitives (writable, readable, derived) form a composable toolkit. A writable store holds mutable state. A readable store exposes values that consumers cannot directly set, with a setup function that controls the value. Derived stores compute values from other stores, creating a dependency graph that updates automatically. The best store designs combine these primitives into custom stores that expose semantic methods (addItem, toggle, clear) rather than raw set/update, encapsulating business logic behind a clean interface.

In Svelte 5, runes in .svelte.js modules offer an alternative to stores for shared state. The choice between them is practical: use runes when you want deep reactivity and simpler syntax for new code; use stores when you need the store contract for interop with SvelteKit's built-in stores ($page, $navigating) or third-party libraries. Both approaches are valid, and most applications will use a mix.

Anti-Patterns

  • Module-Level Stores in SSR — declaring a writable store at the module level without considering that it is a singleton on the server. Two simultaneous SSR requests share the same store, causing data to leak between users. Use context or load function data for request-scoped state.

  • In-Place Mutation Without Reassignment — writing $store.push(item) expecting it to trigger a reactivity update. Svelte stores require reassignment ($store = [...$store, item]) or store.update() to notify subscribers of changes.

  • Using Stores for Parent-Child Communication — creating a global store to pass data between a parent and its direct children when simple props would be clearer, more explicit, and require no subscription management.

  • Forgetting Manual Subscription Cleanup — calling store.subscribe() directly (outside a component's $ shorthand) without storing and calling the returned unsubscribe function, causing memory leaks that accumulate over time.

  • Over-Subscribing in Templates — referencing $store multiple times in a template when a single local variable assignment would suffice. Each $store reference creates a subscription, and for expensive derived stores, this multiplies unnecessary computation.

Overview

Svelte stores are reactive containers for values that can be shared across components. The svelte/store module provides writable, readable, and derived store constructors. In Svelte 5, runes ($state in .svelte.js modules) offer an alternative for shared state, but stores remain essential for working with SvelteKit's built-in stores ($page, $navigating) and for library interoperability.

Core Concepts

Writable Stores

A writable store holds a value that can be read and updated from anywhere.

// src/lib/stores/counter.js
import { writable } from 'svelte/store';

export const count = writable(0);
<script>
  import { count } from '$lib/stores/counter.js';
</script>

<!-- Auto-subscription with $ prefix -->
<p>Count: {$count}</p>

<button onclick={() => count.update(n => n + 1)}>Increment</button>
<button onclick={() => count.set(0)}>Reset</button>

The $ prefix auto-subscribes and unsubscribes when the component is destroyed.

Readable Stores

A readable store exposes a value that consumers cannot directly set.

import { readable } from 'svelte/store';

export const time = readable(new Date(), (set) => {
  const interval = setInterval(() => set(new Date()), 1000);
  return () => clearInterval(interval); // cleanup
});

Derived Stores

Derive a new store from one or more existing stores.

import { derived } from 'svelte/store';
import { items } from './items.js';

export const totalPrice = derived(items, ($items) =>
  $items.reduce((sum, item) => sum + item.price * item.qty, 0)
);

// From multiple stores
import { exchangeRate } from './settings.js';

export const totalInEUR = derived(
  [totalPrice, exchangeRate],
  ([$total, $rate]) => $total * $rate
);

Store Contract

Any object with a subscribe method returning an unsubscribe function is a valid Svelte store and works with the $ prefix. This means you can wrap external state sources:

export function fromEventSource(url) {
  return {
    subscribe(callback) {
      const source = new EventSource(url);
      source.onmessage = (e) => callback(JSON.parse(e.data));
      return () => source.close();
    }
  };
}

Implementation Patterns

Custom Stores with Encapsulated Logic

Wrap a writable store and expose only specific operations:

// src/lib/stores/cart.js
import { writable, derived } from 'svelte/store';

function createCart() {
  const { subscribe, set, update } = writable([]);

  return {
    subscribe,
    addItem(item) {
      update(items => {
        const existing = items.find(i => i.id === item.id);
        if (existing) {
          return items.map(i =>
            i.id === item.id ? { ...i, qty: i.qty + 1 } : i
          );
        }
        return [...items, { ...item, qty: 1 }];
      });
    },
    removeItem(id) {
      update(items => items.filter(i => i.id !== id));
    },
    clear() {
      set([]);
    }
  };
}

export const cart = createCart();

export const cartTotal = derived(cart, ($cart) =>
  $cart.reduce((sum, item) => sum + item.price * item.qty, 0)
);

Persisted Store

Sync a writable store with localStorage:

// src/lib/stores/persisted.js
import { writable } from 'svelte/store';
import { browser } from '$app/environment';

export function persisted(key, initialValue) {
  const stored = browser ? localStorage.getItem(key) : null;
  const data = stored ? JSON.parse(stored) : initialValue;

  const store = writable(data);

  if (browser) {
    store.subscribe((value) => {
      localStorage.setItem(key, JSON.stringify(value));
    });
  }

  return store;
}
export const theme = persisted('theme', 'light');

SvelteKit Built-in Stores

SvelteKit provides several stores via $app/stores:

<script>
  import { page, navigating } from '$app/stores';
</script>

<!-- Current URL, params, data, errors -->
<p>Path: {$page.url.pathname}</p>
<p>User: {$page.data.user?.name}</p>

<!-- Navigation state -->
{#if $navigating}
  <div class="loading-bar" />
{/if}

Svelte 5 Runes as Store Alternative

For new Svelte 5 projects, shared reactive state can be managed with runes in .svelte.js modules:

// src/lib/state/cart.svelte.js
let items = $state([]);

export function addItem(item) {
  const existing = items.find(i => i.id === item.id);
  if (existing) {
    existing.qty++;
  } else {
    items.push({ ...item, qty: 1 });
  }
}

export function removeItem(id) {
  const index = items.findIndex(i => i.id === id);
  if (index !== -1) items.splice(index, 1);
}

export const total = $derived(
  items.reduce((sum, i) => sum + i.price * i.qty, 0)
);

export function getItems() {
  return items;
}

This approach gives you deep reactivity and simpler syntax but does not implement the store contract (no $ prefix auto-subscription in templates — you call functions or reference exported state directly).

Best Practices

  • Use custom stores to encapsulate business logic. Expose semantic methods (addItem, toggle) instead of raw set/update.
  • Prefer derived for computed values rather than subscribing to one store and setting another.
  • Avoid store subscriptions in server-side code. Stores are singletons, which means shared state across requests on the server. Use SvelteKit's locals or load function data instead.
  • Use runes for new Svelte 5 projects when you do not need the store contract for interop. They offer a simpler API and deep reactivity.
  • Clean up manual subscriptions. If you call store.subscribe() directly (outside a component's $ shorthand), you must call the returned unsubscribe function to prevent memory leaks.

Common Pitfalls

  • Shared state across SSR requests. A module-level writable store is a singleton on the server. Two simultaneous requests will share and overwrite the same store value. Use context or load function data for request-scoped state.
  • Forgetting the $ prefix. Referencing count instead of $count in a template gives you the store object, not its value.
  • Over-subscribing. Each $store reference in a component creates a subscription. For expensive derived computations, extract the value once into a local variable.
  • Mutating store values in place. $store.push(item) does not trigger an update. You must reassign: $store = [...$store, item] or use store.update().
  • Using stores where props suffice. If state is only needed by a parent and its direct children, pass it as props rather than creating a global store.

Install this skill directly: skilldb add svelte-skills

Get CLI access →