Svelte and SvelteKit course ยท Module 8: Stores

Custom Stores

2 min read ยท
In this lesson3

Create your own stores with business logic:

Example: Counter Store

// counterStore.js
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),
    set
  };
}

export const counter = createCounter(0);
<script>
  import { counter } from './counterStore.js';
</script>

<p>Count: {$counter}</p>
<button onclick={counter.increment}>+</button>
<button onclick={counter.decrement}>-</button>
<button onclick={counter.reset}>Reset</button>

Example: Warriors Store

// warriorsStore.js
import { writable, derived } from 'svelte/store';

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

  return {
    subscribe,
    add: (warrior) => update(warriors => [...warriors, {
      id: Date.now(),
      ...warrior
    }]),
    remove: (id) => update(warriors =>
      warriors.filter(w => w.id !== id)
    ),
    updateStrength: (id, strength) => update(warriors =>
      warriors.map(w => w.id === id ? { ...w, strength } : w)
    ),
    reset: () => set([])
  };
}

export const warriors = createWarriorsStore();

// Derived stores
export const totalWarriors = derived(warriors, $w => $w.length);
export const totalStrength = derived(warriors,
  $w => $w.reduce((sum, w) => sum + w.strength, 0)
);

Store with localStorage

// settingsStore.js
import { writable } from 'svelte/store';

function createPersistentStore(key, initial) {
  // SSR (SvelteKit): there is no localStorage on the server
  const storage = typeof localStorage !== 'undefined' ? localStorage : null;
  const stored = storage?.getItem(key);
  const data = stored ? JSON.parse(stored) : initial;

  const { subscribe, set, update } = writable(data);

  return {
    subscribe,
    set: (value) => {
      storage?.setItem(key, JSON.stringify(value));
      set(value);
    },
    update: (fn) => {
      update(current => {
        const newValue = fn(current);
        storage?.setItem(key, JSON.stringify(newValue));
        return newValue;
      });
    }
  };
}

export const settings = createPersistentStore('settings', {
  theme: 'dark',
  language: 'pl'
});

The typeof localStorage check keeps the module from failing during server rendering in SvelteKit - there the store keeps its initial value.

Code for this lesson: App.svelte
<script>
  // Custom store with methods
  import { warriors, addWarrior, removeWarrior, stats } from './stores.js';

  let newName = $state('');

  function handleAdd() {
    if (newName.trim()) {
      addWarrior(newName);
      newName = '';
    }
  }
</script>

<main class="warriors-app">
  <h1>Custom Store</h1>
  <p class="subtitle">Store with custom methods</p>

  <section>
    <h2>Add warrior</h2>
    <div class="add-form">
      <input
        bind:value={newName}
        placeholder="Warrior name"
        onkeydown={(e) => e.key === 'Enter' && handleAdd()}
      />
      <button onclick={handleAdd}>Add</button>
    </div>
  </section>

  <section>
    <h2>Warrior list ({$warriors.length})</h2>
    {#each $warriors as warrior (warrior.id)}
      <div class="warrior-row">
        <span>{warrior.name}</span>
        <span class="health">{warrior.health}</span>
        <button onclick={() => removeWarrior(warrior.id)} aria-label="Remove {warrior.name}">ร—</button>
      </div>
    {:else}
      <p class="empty">No warriors</p>
    {/each}
  </section>

  <section class="stats">
    <h2>Statistics (derived)</h2>
    <div class="stat-grid">
      <div class="stat">
        <span>Total</span>
        <strong>{$stats.total}</strong>
      </div>
      <div class="stat">
        <span>Total HP</span>
        <strong>{$stats.totalHealth}</strong>
      </div>
      <div class="stat">
        <span>Average HP</span>
        <strong>{$stats.avgHealth}</strong>
      </div>
    </div>
  </section>
</main>

<style>
  .warriors-app {
    font-family: 'Georgia', serif;
    max-width: 500px;
    margin: 0 auto;
    padding: 2rem;
    background: #1a0f0a;
    color: #e8d5b7;
    min-height: 100vh;
  }

  h1 {
    color: #d4af37;
    text-align: center;
  }

  .subtitle {
    text-align: center;
    color: #8b7355;
  }

  section {
    background: rgba(212, 175, 55, 0.1);
    border: 1px solid #d4af37;
    border-radius: 8px;
    padding: 1rem;
    margin: 1rem 0;
  }

  h2 {
    color: #d4af37;
    font-size: 1rem;
    margin-top: 0;
  }

  .add-form {
    display: flex;
    gap: 0.5rem;
  }

  input {
    flex: 1;
    padding: 0.5rem;
    background: rgba(0,0,0,0.3);
    border: 1px solid #3d2914;
    border-radius: 4px;
    color: #e8d5b7;
  }

  button {
    padding: 0.5rem 1rem;
    background: #3d2914;
    color: #e8d5b7;
    border: 1px solid #d4af37;
    border-radius: 4px;
    cursor: pointer;
  }

  .warrior-row {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.5rem;
    background: rgba(0,0,0,0.3);
    margin: 0.25rem 0;
    border-radius: 4px;
  }

  .warrior-row span:first-child {
    flex: 1;
  }

  .health {
    color: #ff6b6b;
  }

  .warrior-row button {
    background: #8b0000;
    border-color: #8b0000;
    padding: 0.25rem 0.5rem;
  }

  .empty {
    text-align: center;
    color: #8b7355;
  }

  .stat-grid {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 0.5rem;
  }

  .stat {
    text-align: center;
    padding: 0.5rem;
    background: rgba(0,0,0,0.3);
    border-radius: 4px;
  }

  .stat span {
    display: block;
    font-size: 0.85rem;
    color: #8b7355;
  }

  .stat strong {
    font-size: 1.25rem;
    color: #d4af37;
  }
</style>

Docs entries used in this lesson

Spotted a mistake in this lesson?

Check yourself

Answer the questions from this lesson. Pick an answer to see right away whether it is correct.

  1. 1. What must a custom store export to be compatible with the $ syntax?

Useful articles