# Svelte 5 State Management Guide
## Overview
Svelte 5 provides multiple patterns for state management, from local component state to global application state. Choose the right pattern based on your needs.
---
## 📊 State Management Decision Tree
```
Is the state used by only one component?
├── YES → Use local $state()
└── NO → Is it shared between parent/child?
├── YES → Pass as props or use $bindable()
└── NO → Is it shared across unrelated components?
├── YES → Use a .svelte.ts store
└── NO → Is it server data that needs caching?
└── YES → Use SvelteKit load functions
```
---
## 🏠 Pattern 1: Local Component State
For state that belongs to a single component.
```svelte
Count: {count}
Average: {average.toFixed(2)}
+1
Reset
```
---
## 📤 Pattern 2: Props Down, Events Up
Classic unidirectional data flow.
```svelte
```
---
## 🔗 Pattern 3: Two-Way Binding with $bindable
For controlled form components.
```svelte
```
---
## 🏪 Pattern 4: Global Stores (.svelte.ts files)
For state shared across unrelated components.
```typescript
// stores/auth.svelte.ts
export interface User {
id: string;
name: string;
email: string;
avatar?: string;
}
class AuthStore {
user = $state(null);
loading = $state(false);
error = $state(null);
isAuthenticated = $derived(this.user !== null);
async login(email: string, password: string) {
this.loading = true;
this.error = null;
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
});
if (!response.ok) {
throw new Error('Login failed');
}
this.user = await response.json();
} catch (e) {
this.error = e instanceof Error ? e.message : 'Unknown error';
throw e;
} finally {
this.loading = false;
}
}
async logout() {
await fetch('/api/auth/logout', { method: 'POST' });
this.user = null;
}
async checkSession() {
try {
const response = await fetch('/api/auth/me');
if (response.ok) {
this.user = await response.json();
}
} catch {
this.user = null;
}
}
}
export const auth = new AuthStore();
```
```svelte
{#if auth.isAuthenticated}
Welcome, {auth.user?.name}
auth.logout()}>Logout
{:else}
Login
{/if}
```
---
## 🛒 Pattern 5: Complex Store with Actions
For stores with complex business logic.
```typescript
// stores/cart.svelte.ts
export interface CartItem {
id: string;
name: string;
price: number;
quantity: number;
}
class CartStore {
items = $state([]);
// Derived values
itemCount = $derived(
this.items.reduce((sum, item) => sum + item.quantity, 0)
);
subtotal = $derived(
this.items.reduce((sum, item) => sum + item.price * item.quantity, 0)
);
tax = $derived(this.subtotal * 0.1);
total = $derived(this.subtotal + this.tax);
isEmpty = $derived(this.items.length === 0);
// Actions
addItem(product: { id: string; name: string; price: number }) {
const existing = this.items.find(item => item.id === product.id);
if (existing) {
existing.quantity++;
} else {
this.items.push({
...product,
quantity: 1
});
}
this.persist();
}
removeItem(id: string) {
const index = this.items.findIndex(item => item.id === id);
if (index !== -1) {
this.items.splice(index, 1);
this.persist();
}
}
updateQuantity(id: string, quantity: number) {
const item = this.items.find(item => item.id === id);
if (item) {
if (quantity <= 0) {
this.removeItem(id);
} else {
item.quantity = quantity;
this.persist();
}
}
}
clear() {
this.items = [];
this.persist();
}
// Persistence
private persist() {
if (typeof localStorage !== 'undefined') {
localStorage.setItem('cart', JSON.stringify($state.snapshot(this.items)));
}
}
hydrate() {
if (typeof localStorage !== 'undefined') {
const saved = localStorage.getItem('cart');
if (saved) {
try {
this.items = JSON.parse(saved);
} catch {
this.items = [];
}
}
}
}
}
export const cart = new CartStore();
```
```svelte
🛒
{#if cart.itemCount > 0}
{cart.itemCount}
{/if}
Subtotal: ${cart.subtotal.toFixed(2)}
Tax: ${cart.tax.toFixed(2)}
Total: ${cart.total.toFixed(2)}
```
---
## 🌐 Pattern 6: Context for Component Trees
Pass state down a component tree without prop drilling.
```svelte
{@render children()}
```
```svelte
setTheme(theme.value === 'dark' ? 'light' : 'dark')}>
{theme.value === 'dark' ? '☀️' : '🌙'}
```
---
## 📡 Pattern 7: Async State with Loading/Error
Handle async operations elegantly.
```typescript
// stores/async.svelte.ts
export type AsyncState =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: Error };
export function createAsyncState() {
let state = $state>({ status: 'idle' });
return {
get state() { return state; },
get isLoading() { return state.status === 'loading'; },
get isSuccess() { return state.status === 'success'; },
get isError() { return state.status === 'error'; },
get data() { return state.status === 'success' ? state.data : null; },
get error() { return state.status === 'error' ? state.error : null; },
async run(fn: () => Promise) {
state = { status: 'loading' };
try {
const data = await fn();
state = { status: 'success', data };
return data;
} catch (e) {
const error = e instanceof Error ? e : new Error(String(e));
state = { status: 'error', error };
throw error;
}
},
reset() {
state = { status: 'idle' };
}
};
}
// Usage in a store
class UserStore {
private usersAsync = createAsyncState();
get users() { return this.usersAsync.data ?? []; }
get loading() { return this.usersAsync.isLoading; }
get error() { return this.usersAsync.error; }
async fetchUsers() {
await this.usersAsync.run(async () => {
const res = await fetch('/api/users');
return res.json();
});
}
}
```
---
## 🔄 Pattern 8: Undo/Redo State
Maintain history for undo/redo functionality.
```typescript
// stores/history.svelte.ts
export function createHistoryState(initial: T) {
let past = $state([]);
let present = $state(initial);
let future = $state([]);
return {
get value() { return present; },
get canUndo() { return past.length > 0; },
get canRedo() { return future.length > 0; },
set(value: T) {
past.push($state.snapshot(present) as T);
present = value;
future = [];
},
undo() {
if (past.length === 0) return;
future.unshift($state.snapshot(present) as T);
present = past.pop()!;
},
redo() {
if (future.length === 0) return;
past.push($state.snapshot(present) as T);
present = future.shift()!;
},
reset(value: T) {
past = [];
present = value;
future = [];
}
};
}
// Usage
const editor = createHistoryState({ text: '', fontSize: 16 });
// Make changes
editor.set({ text: 'Hello', fontSize: 16 });
editor.set({ text: 'Hello World', fontSize: 16 });
// Undo
editor.undo(); // Back to 'Hello'
editor.undo(); // Back to ''
// Redo
editor.redo(); // Forward to 'Hello'
```
---
## 📋 State Management Best Practices
### ✅ Do:
- Keep state as local as possible
- Use `$derived` for computed values
- Use `.svelte.ts` files for shared state
- Type your state with TypeScript
- Use `$state.snapshot()` before serializing
- Clean up effects that create subscriptions
### ❌ Don't:
- Create global stores for component-local state
- Mutate derived values
- Put async logic in `$effect` without cleanup
- Destructure `$state` objects (breaks reactivity)
- Forget to handle loading/error states
---
## 🔍 Debugging State
```svelte
```