Imagine you build a counter button. Click it → the number goes up. Easy.
Then you add another button in a different part of the UI that’s supposed to control the same counter. You click one… it goes up. You click the other… nothing happens, because it’s got its own copy of the state.
This is like two people keeping score on different napkins. You write “5,” they write “3,” and now you’re arguing about who’s right.
What we need is a shared whiteboard 📝 — one place to keep the truth, so everyone’s on the same page.
That’s exactly what stores are in Svelte: a way to hold reactive values outside of any single component.
What is a store?
A store is just a special object that holds a value and notifies subscribers when it changes.
- Any component can read from it.
- Any component can write to it (if it’s writable).
- And best of all, components update automatically when the store changes.
Think of it like a radio station 📻. The store broadcasts the current song (value), and every component listening gets the same tune instantly.
Creating your first writable store
The most common type of store is a writable store — one you can both read and update.
Let’s create one. We’ll keep our stores in a separate file so they’re easy to import anywhere.
📂 src/lib/stores.js
import { writable } from 'svelte/store';
// start our counter at 0
export const count = writable(0);
That’s it — we’ve got a store named count
. Notice we export
it so other files can import and use the same value.
Reading from the store
Let’s build a component that reads the store and shows the value.
📂 src/lib/CounterDisplay.svelte
<script>
import { count } from './stores.js';
</script>
<p>The count is: {$count}</p>
That $count
is Svelte’s auto-subscription syntax:
- When this component mounts, it subscribes to
count
. - Whenever the value changes, the
<p>
updates automatically. - When the component unmounts, Svelte unsubscribes for us.
No boilerplate. No cleanup. Just $count
.
To see it in action, let’s add this component to our page.
📂 src/routes/+page.svelte
<script>
import CounterDisplay from '$lib/CounterDisplay.svelte';
</script>
<h1>Shared Counter Demo</h1>
<CounterDisplay />
Load the page and you’ll see: “The count is: 0”. (no increment button yet)
Updating the store
Of course, staring at 0
isn’t very exciting. Let’s add some controls to change the store’s value.
A writable store has two methods:
-
.set(newValue)
→ replace the value. -
.update(fn)
→ calculate a new value based on the old one.
📂 src/lib/CounterControls.svelte
<script>
import { count } from './stores.js';
</script>
<button onclick={() => count.update(n => n + 1)}>
Increment
</button>
<button onclick={() => count.set(0)}>
Reset
</button>
And use it in the page:
📂 src/routes/+page.svelte
<script>
import CounterDisplay from '$lib/CounterDisplay.svelte';
import CounterControls from '$lib/CounterControls.svelte';
</script>
<h1>Shared Counter Demo</h1>
<CounterDisplay />
<CounterControls />
Now you can increment and reset the count, and the display updates automatically. 🎉
The shorthand way
Since $count
is reactive, you can also assign to it directly, just like a rune variable. Svelte turns $count = ...
into a store update under the hood.
📂 src/lib/CounterControls.svelte
<script>
import { count } from './stores.js';
</script>
<button onclick={() => $count++}>
Increment
</button>
<button onclick={() => $count = 0}>
Reset
</button>
When you do this, Svelte quietly updates the store behind the scenes. So even though $count looks like an ordinary variable, it’s really wiring into the store for you.
👉 Under the hood, these assignments get turned into calls to special methods like .set and .update. Don’t worry if that doesn’t make sense yet — we’ll dig into those methods in the next section. For now, just know that $count can be both a reader and a writer.
This shorthand is what you’ll see in most real projects because it keeps the code clean and natural. And since it’s all one shared store, your will instantly reflect the changes.
Multiple components, one store
The real superpower of stores is that many components can share them at once. Let’s make another button component that also increments the counter.
📂 src/lib/CounterButton.svelte
<script>
import { count } from './stores.js';
</script>
<button onclick={() => $count++}>
Add one (current: {$count})
</button>
Now drop it into the page along with the others:
📂 src/routes/+page.svelte
<script>
import CounterDisplay from '$lib/CounterDisplay.svelte';
import CounterControls from '$lib/CounterControls.svelte';
import CounterButton from '$lib/CounterButton.svelte';
</script>
<h1>Shared Counter Demo</h1>
<CounterDisplay />
<CounterControls />
<CounterButton />
Click any of the buttons, and watch how the display and both buttons all stay perfectly in sync. That’s the shared whiteboard effect.
When should you use a store?
Quick rule of thumb:
- Use
$state
for local state that only matters to one component. - Use a store when multiple components need to share the same truth.
Stores are your app’s bulletin board 📌. Post something once, and everyone can see the update.
Readable stores
Imagine a clock ⏰. Every second it ticks forward.
Do we really want every component to have the power to set the clock? No way. We only want them to read it.
That’s what readable stores are for: stores that provide values but don’t let consumers overwrite them.
📂 src/lib/time.js
(note .js, not .svelte)
import { readable } from 'svelte/store';
// create a readable store that updates every second
export const time = readable(new Date(), (set) => {
const interval = setInterval(() => {
set(new Date());
}, 1000);
// cleanup when no one is using it
return () => clearInterval(interval);
});
Let’s break this down:
- We’re in a plain
.js
file, not.svelte
, because this store doesn’t render anything. It’s just a little data factory that other components can import. -
readable
takes two arguments:
- The initial value → here
new Date()
. - A function that tells Svelte how to keep the store updated. It gets a special
set
callback we can call whenever we want to push a new value.- Inside, we use
setInterval
to callset(new Date())
every second. That keeps the store ticking. - Finally, we return a cleanup function. This is important: if no component is subscribed anymore, Svelte calls this function to free resources. In this case, it clears the interval so we don’t waste memory or battery.
- Inside, we use
So time
is a read-only stream of values: components can subscribe to it, but they can’t assign to it or call .set
themselves.
Now let’s use it.
📂 src/lib/Clock.svelte
<script>
import { time } from './time.js';
</script>
<p>The time is: {$time.toLocaleTimeString()}</p>
📂 src/routes/+page.svelte
<script>
import Clock from '$lib/Clock.svelte';
</script>
<h1>Shared Clock Demo</h1>
<Clock />
Now your page shows a live clock along with the counter. The Clock
can read time
, but it can’t mess with it. Perfect.
Derived stores
What if we want a value that’s calculated from one or more stores? Enter derived stores.
Think about a shopping cart 🛒. You’ve got an array of items, each with a price. You also want to display the total cost. The total depends on the items, so instead of recalculating manually everywhere, let’s make a derived store.
📂 src/lib/cart.js
import { writable, derived } from 'svelte/store';
// array of items in the cart
export const cart = writable([
{ name: 'Apples', price: 2 },
{ name: 'Bananas', price: 3 }
]);
// derived store: sum of item prices
export const total = derived(cart, ($cart) =>
$cart.reduce((sum, item) => sum + item.price, 0)
);
Here’s what’s happening:
-
cart
is our base store, holding an array of items. -
total
is a derived store.- It takes
cart
as input. - The arrow function
( $cart ) => …
runs whenevercart
changes. - Here, .reduce is a regular JavaScript array method (not part of Svelte) that adds up all the item prices into one number. Here, it walks through every item in the array and keeps a running total of the price values. We start from 0 and add each item’s price one by one.
- Whatever that function returns becomes the new value of
total
.
- It takes
So total
is always kept up to date — if the cart changes, the total recalculates automatically.
Now let’s display it:
📂 src/lib/CartSummary.svelte
<script>
import { cart, total } from './cart.js';
</script>
<h2>Cart</h2>
<ul>
{#each $cart as item}
<li>{item.name} — ${item.price}</li>
{/each}
</ul>
<p>Total: ${$total}</p>
📂 src/routes/+page.svelte
<script>
import CartSummary from '$lib/CartSummary.svelte';
</script>
<CartSummary />
Now your UI shows a live-updating cart total, automatically recalculated whenever cart
changes.
💡 Mini challenge: try adding a way to change quantities or add new items to the cart, and watch how the total store updates all by itself.
Stores vs Props
At this point you might be thinking: “Why not just pass values down as props?” Good question.
Here’s the rule of thumb:
- Props are perfect when you pass data from a parent to its direct children.
- Stores are better when many components across your app need to share the same state, especially if they’re not directly related in the tree.
In other words: Use props until it hurts. When it does, reach for a store.
Props are like handing notes directly to your kids. Stores are like putting it on the family fridge where everyone in the house can see.
Mini Project — A Todo App with Stores
Instead of throwing together a bunch of unrelated demos, let’s build one cohesive little app: a Todo List.
This will showcase everything we’ve learned so far:
- A writable store to hold the todos.
- A derived store to calculate how many are completed.
- A readable store for static app info like version number.
Step 1: Define our stores
📂 src/lib/todos.js
import { writable, derived, readable } from 'svelte/store';
// writable: our list of todos
export const todos = writable([
{ text: 'Learn stores', done: true },
{ text: 'Build a Svelte app', done: false }
]);
// derived: count how many are done
export const completedCount = derived(todos, ($todos) =>
$todos.filter(todo => todo.done).length
);
// readable: static app info
export const appVersion = readable('v1.0.0');
What’s going on here?
-
todos
is a writable store. Any component can both read ($todos
) and update it. -
completedCount
is a derived store. It watchestodos
and automatically recalculates whenever the todos change. -
appVersion
is a readable store. It’s just a constant value that can’t be overwritten from outside.
Step 2: A component to manage todos
📂 src/lib/TodoList.svelte
<script>
import { todos } from './todos.js';
let newTodo = '';
function addTodo() {
if (newTodo.trim()) {
$todos = [...$todos, { text: newTodo, done: false }];
newTodo = '';
}
}
function toggleTodo(index) {
$todos = $todos.map((todo, i) =>
i === index ? { ...todo, done: !todo.done } : todo
);
}
</script>
<h2>Todos</h2>
<input bind:value={newTodo} placeholder="New todo" />
<button onclick={addTodo}>Add</button>
<ul>
{#each $todos as todo, i}
<li>
<label>
<input type="checkbox" checked={todo.done} onclick={() => toggleTodo(i)} />
{todo.text}
</label>
</li>
{/each}
</ul>
Breakdown:
- We import the
todos
store. -
newTodo
is just local state for the input box. -
addTodo()
uses$todos = ...
shorthand to add a new item. -
toggleTodo()
flips thedone
status of a todo at a given index. - The
<ul>
shows every todo, with checkboxes that stay in sync thanks to the store.
Every time we change $todos
, any component using it updates automatically.
Step 3: A component to show summary info
📂 src/lib/TodoSummary.svelte
<script>
import { todos, completedCount, appVersion } from './todos.js';
</script>
<p>
Completed: {$completedCount} / {$todos.length}
</p>
<p>
App version: {$appVersion}
</p>
Breakdown:
-
$completedCount
updates whenevertodos
changes — no manual math. -
$todos.length
shows the total number of tasks. -
$appVersion
comes from our readable store. Nothing can overwrite it, so it’s guaranteed to stay consistent.
Step 4: Bring it together
📂 src/routes/+page.svelte
<script>
import TodoList from '$lib/TodoList.svelte';
import TodoSummary from '$lib/TodoSummary.svelte';
</script>
<h1>Todo App with Stores</h1>
<TodoList />
<TodoSummary />
Now we’ve got a full mini app:
- Add todos.
- Toggle them complete/incomplete.
- Watch the completed count update instantly.
- See a static app version displayed at the bottom.
Why this is powerful
This tiny Todo app shows all three store types working together in a realistic scenario:
- The todos are shared state (multiple components read/write them).
- The completed count is derived automatically.
- The app version is read-only.
This is exactly how you’d structure state in a real-world app: writable for user data, derived for computed values, and readable for constants.
Recap
In this article we’ve gone far beyond local state:
- Writable stores → values you can read and update from anywhere.
- Readable stores → values that components can only listen to, not change.
- Derived stores → computed values that update automatically when dependencies change.
- When to use stores vs props → props for local communication, stores for global/shared truths.
- A full mini project combining them.
Stores are the glue that keeps state consistent across your Svelte app.
Next up, we’ll look at context — another way to share data, but in a more scoped way — and then we’ll bring in async data fetching. That’s where things start feeling like a real-world app.
Follow me on DEV for future posts in this deep-dive series.
https://dev.to/a1guy
If it helped, leave a reaction (heart / bookmark) — it keeps me motivated to create more content
Checkout my offering on YouTube with (growing) crash courses and content on JavaScript, React, TypeScript, Rust, WebAssembly, AI Prompt Engineering and more: @LearnAwesome
Top comments (0)