Svelte and SvelteKit course ยท Module 8: Stores
Custom Stores
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. What must a custom store export to be compatible with the $ syntax?