Svelte and SvelteKit course ยท Module 8: Stores
Stores vs $state
In this lesson6
In Svelte 5 you can keep shared state in stores or in runes. When should you pick which?
$state - Component State
<script>
// Local component state
let count = $state(0);
let items = $state([]);
</script>Use when:
- State is local to the component
- You don't need access from other components
- Data doesn't need to survive unmounting
$state in a .svelte.js File - Shared State
Runes also work outside components, in files ending with .svelte.js (or .svelte.ts). This is the way Svelte 5 recommends for shared state in new code:
// treasury.svelte.js
export const treasury = $state({ gold: 0 });
export function addGold(amount) {
treasury.gold += amount;
}<!-- Chest.svelte -->
<script>
import { treasury, addGold } from './treasury.svelte.js';
</script>
<p>Gold: {treasury.gold}</p>
<button onclick={() => addGold(10)}>+10 gold</button>Every component that imports treasury sees the same gold - with no $ before the name and no subscription. Export an object and change its fields: a $state variable that you assign a new value to cannot be exported directly (the compiler reports an error).
In SvelteKit such a module is shared by the whole server, so pass a specific user's data through context (setContext, lesson 4_5), not through the module.
Stores - Still Valid
// stores.js
import { writable } from 'svelte/store';
export const user = writable(null);
export const cart = writable([]);Use when:
- You have complex, asynchronous data streams (WebSocket, timers)
- You need manual control over the subscription (start and stop in
readable) - A library or older code provides a store
Context + $state
<!-- Provider.svelte -->
<script>
import { setContext } from 'svelte';
let { children } = $props();
let state = $state({ user: null, theme: 'dark' });
setContext('app', {
get state() { return state; },
setUser: (user) => state.user = user,
toggleTheme: () => state.theme = state.theme === 'dark' ? 'light' : 'dark'
});
</script>
{@render children()}Comparison
| Feature | $state in a component | $state in .svelte.js | Stores |
|---|---|---|---|
| Scope | Component | Whole app | Whole app |
| Reading in the template | {count} | {treasury.gold} | {$gold} |
| Access outside components | No | Yes | Yes (subscribe, get) |
| In new code | Yes | Yes | For streams and libraries |
Combining Stores and Runes
You can read a store with the $ prefix inside runes too, for example in $derived:
<script>
import { warriors } from './stores.js';
// $derived recalculates when the store changes
let activeWarriors = $derived($warriors.filter(w => w.active));
</script>
<p>Active warriors: {activeWarriors.length}</p>Store values computed from a store with $derived, not with an $effect that assigns - the result is always up to date and there is no risk of an effect loop.
Code for this lesson: App.svelte
<script>
// Svelte 5: $state, $state in a .svelte.js file and stores - when to use what?
import { globalCount } from './stores.js';
import { shared } from './shared.svelte.js';
import Watcher from './Watcher.svelte';
// Local state - only in this component
let localCount = $state(0);
</script>
<main class="comparison">
<h1>$state vs Stores</h1>
<section>
<h2>$state (local)</h2>
<p>Value: <strong>{localCount}</strong></p>
<button onclick={() => localCount++}>+1 locally</button>
<p class="hint">Only in this component</p>
</section>
<section>
<h2>$state in shared.svelte.js (shared)</h2>
<p>Value: <strong>{shared.count}</strong></p>
<button onclick={() => shared.count++}>+1 shared</button>
<p class="hint">The Svelte 5 way for new code - no $ and no subscription</p>
</section>
<section>
<h2>Store (shared)</h2>
<p>Value: <strong>{$globalCount}</strong></p>
<button onclick={() => globalCount.update(n => n + 1)}>+1 in the store</button>
<p class="hint">Still valid: data streams, libraries, older code</p>
</section>
<Watcher />
<section class="guidelines">
<h2>When to use what?</h2>
<div class="guideline">
<h3>$state</h3>
<ul>
<li>Component local state</li>
<li>Shared state in .svelte.js files</li>
<li>Forms, UI state (hover, open)</li>
</ul>
</div>
<div class="guideline">
<h3>Stores</h3>
<ul>
<li>Asynchronous data streams</li>
<li>Manual control of subscriptions</li>
<li>Libraries and older code</li>
</ul>
</div>
</section>
</main>
<style>
.comparison {
font-family: 'Georgia', serif;
max-width: 600px;
margin: 0 auto;
padding: 2rem;
background: #1a0f0a;
color: #e8d5b7;
min-height: 100vh;
}
h1 {
color: #d4af37;
text-align: center;
}
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;
}
strong {
color: #ffd700;
font-size: 1.5rem;
}
button {
padding: 0.5rem 1rem;
background: #3d2914;
color: #e8d5b7;
border: 1px solid #d4af37;
border-radius: 4px;
cursor: pointer;
}
.hint {
font-size: 0.85rem;
color: #8b7355;
font-style: italic;
margin-top: 0.5rem;
}
.guidelines {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
.guidelines h2 {
grid-column: 1 / -1;
}
.guideline {
background: rgba(0,0,0,0.3);
padding: 1rem;
border-radius: 4px;
}
.guideline h3 {
color: #d4af37;
margin: 0 0 0.5rem 0;
font-size: 1rem;
}
.guideline ul {
margin: 0;
padding-left: 1.2rem;
}
.guideline li {
margin: 0.25rem 0;
font-size: 0.9rem;
}
</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. When is it better to use Stores instead of $state?