or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

examples

edge-cases.mdreal-world-scenarios.md
index.md
tile.json

stores.mddocs/reference/

Svelte Store System

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.

Overview

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.

Core Types

Subscriber<T>

Callback function invoked when a store's value changes.

type Subscriber<T> = (value: T) => void;

Parameters:

  • value: The new value of the store

Description: This callback is invoked immediately upon subscription with the current value, and subsequently whenever the store's value changes.

Unsubscriber

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.

Updater<T>

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 store

Returns: 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.

StartStopNotifier<T>

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 directly
  • update: Function to update the store's value via an updater function

Returns: 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);
  };
});

Store Interfaces

Readable<T>

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:

subscribe()

Subscribes to store value changes.

Parameters:

  • run: Callback invoked immediately with the current value and on each subsequent change
  • invalidate (optional): Callback invoked just before run is called, useful for cleanup

Returns: Unsubscriber function to cancel the subscription

Example:

const unsubscribe = store.subscribe(value => {
  console.log('Store value:', value);
});

// Later, when done
unsubscribe();

Writable<T>

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:

set()

Replaces the store's value and notifies all subscribers.

Parameters:

  • value: The new value to set

Example:

count.set(42);

update()

Updates the store's value based on its current value.

Parameters:

  • updater: Function that receives the current value and returns the new value

Example:

count.update(n => n + 1);

Store Creation Functions

writable()

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 store
  • start (optional): Function called when the first subscriber subscribes

Returns: 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>

readable()

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 store
  • start (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();
});

derived()

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 from
  • fn: Function that computes the derived value
    • For synchronous derivation: receives store values, returns new value
    • For asynchronous derivation: receives store values, set, and update functions
  • initial_value (optional): Initial value before the first derivation completes

Returns: 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
);

readonly()

Wraps a store to make it read-only.

function readonly<T>(store: Readable<T>): Readable<T>;

Parameters:

  • store: A readable store to make read-only

Returns: 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>

get()

Synchronously retrieves the current value from a store.

function get<T>(store: Readable<T>): T;

Parameters:

  • store: A readable store

Returns: 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.

Store-Rune Interoperability

toStore()

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 value
  • set (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} />

fromStore()

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 store

Returns: 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>

Common Patterns

Custom Store

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>

Debounced Store

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);

Local Storage Sync

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'
});

Store Composition

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
  })
);

Async Store with Loading State

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 loaded

Computed Store with Multiple Dependencies

import { 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
    };
  }
);

Store with Middleware

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 4

Migration from Runes to Stores

When 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>

Migration from Stores to Runes

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>

Best Practices

1. Use Meaningful Names

// Bad
const s = writable(0);

// Good
const userCount = writable(0);
const isAuthenticated = writable(false);
const activeUsers = writable([]);

2. Keep Stores Simple

// 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);
  }
}

3. Use Derived Stores for Computed Values

// 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}`
);

4. Cleanup Subscriptions

// 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

5. Use Readonly for Encapsulation

// 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 }));
}

6. Prefer Stores for Shared State

Use stores when:

  • State is shared across multiple components
  • State needs to persist between component mount/unmount
  • You need reactive subscriptions outside components
  • Integrating with external libraries

Use runes ($state) when:

  • State is local to a single component
  • Building new Svelte 5+ applications
  • You want simpler, more direct syntax

7. Initial Values

// 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'
);

8. Avoid Overusing get()

// 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);

Performance Considerations

1. Derived Store Memoization

Derived stores only recalculate when their dependencies change:

const expensiveComputation = derived(
  sourceStore,
  $source => {
    // This only runs when sourceStore changes
    return performExpensiveOperation($source);
  }
);

2. Avoid Unnecessary Subscriptions

// 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);
  });
}

3. Batch Updates

// 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);

4. Lazy Initialization

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);
  };
});

Type Safety with TypeScript

Basic Usage

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
);

Custom Store Types

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)
  };
}

Generic Stores

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']);

See Also

  • Svelte Reactivity - Runes and reactive classes
  • Svelte Motion - Animated stores with Spring and Tween
  • Component Lifecycle - Using stores with lifecycle functions
  • $state Rune - Modern alternative to stores for local state
  • $derived Rune - Modern alternative to derived stores

Version: Svelte 5.46.1 Module: svelte/store