Modern reactive state management without the boilerplate. Svelte 5 replaces stores with runes for cleaner, more intuitive code.
🎯 Quick Start
<script>
// State — reactive variable
let count = $state(0)
// Derived — computed value
let doubled = $derived(count * 2)
// Effect — side effect
$effect(() => {
console.log('Count changed:', count)
})
</script>
<button onclick={() => count++}>
Clicks: {count} (×2 = {doubled})
</button>📦 Core Runes
$state — Reactive State
Creates reactive state that triggers updates when mutated.
<script>
// Primitive
let count = $state(0)
// Object — deeply reactive
let user = $state({
name: 'Ada',
settings: { theme: 'dark' }
})
// Array — mutations reactive
let items = $state(['a', 'b'])
function add() {
items.push('c') // ✅ Reactive
items = [...items] // ✅ Also works
}
</script>Key behaviors:
- Objects/arrays are deeply reactive
- Reassignment triggers updates
- Mutations on $state objects are tracked
$derived — Computed Values
Derived state that updates automatically when dependencies change.
<script>
let items = $state([1, 2, 3])
// Simple derivation
let sum = $derived(items.reduce((a, b) => a + b, 0))
// Conditional derivation
let status = $derived(
items.length === 0 ? 'empty' :
items.length < 5 ? 'few' : 'many'
)
// Derived objects
let stats = $derived({
count: items.length,
total: sum,
average: sum / items.length
})
</script>Rules:
- Never assign to $derived values
- Must be pure (no side effects)
- Updates synchronously
$effect — Side Effects
Runs when dependencies change. For DOM operations, subscriptions, etc.
<script>
let count = $state(0)
let element = $state() // bound element
// Runs on every count change
$effect(() => {
document.title = `Count: ${count}`
})
// Runs when element is bound
$effect(() => {
if (element) {
element.focus()
}
})
// Cleanup with untrack
$effect(() => {
const id = setInterval(() => count++, 1000)
return () => clearInterval(id) // cleanup
})
</script>
<div bind:this={element} />Pre-runes equivalent:
| Old | New |
|---|---|
onMount | $effect with DOM check |
beforeUpdate/afterUpdate | $effect.pre / $effect |
onDestroy | cleanup function in $effect |
$props — Component Props
Declare and destructure props with reactivity.
<script>
// Basic
let { name, age = 18 } = $props()
// With TypeScript
interface Props {
name: string
age?: number
onSelect: (id: string) => void
}
let { name, age = 18, onSelect }: Props = $props()
// Rest props
let { class: className, ...rest } = $props()
</script>
<div class={className} {...rest} />$bindable / bind: — Two-way Binding
<!-- Parent.svelte -->
<script>
let value = $state('')
</script>
<Input bind:value />
<!-- Input.svelte -->
<script>
let { value = $bindable() } = $props()
</script>
<input bind:value />Patterns:
<!-- Form binding -->
<input bind:value={text} />
<input type="checkbox" bind:checked={enabled} />
<!-- Element binding -->
<div bind:this={element} />
<div bind:clientWidth={width} />
<!-- Component binding (requires $bindable) -->
<Slider bind:value={volume} />🧩 Advanced Patterns
Shared State (Module-level)
// stores/ui.svelte.ts
export const ui = $state({
sidebarOpen: false,
theme: "dark" as "light" | "dark",
toasts: [] as Toast[],
})
export function toggleSidebar() {
ui.sidebarOpen = !ui.sidebarOpen
}
export function addToast(message: string) {
ui.toasts.push({ id: crypto.randomUUID(), message })
}Import anywhere:
<script>
import { ui, toggleSidebar } from './stores/ui.svelte'
</script>
<button onclick={toggleSidebar}>
{ui.sidebarOpen ? 'Close' : 'Open'}
</button>Class State Management
// services/Counter.svelte.ts
export class Counter {
count = $state(0)
increment() {
this.count++
}
get doubled() {
return $derived(this.count * 2)
}
}
// Usage
const counter = new Counter()Untrack / Isolate
<script>
import { untrack } from 'svelte'
let a = $state(0)
let b = $state(0)
$effect(() => {
// Only re-runs when 'a' changes
console.log(a, untrack(() => b))
})
</script>🔄 Migration Guide
From Svelte 4 Stores
| Store Pattern | Rune Equivalent |
|---|---|
writable(0) | $state(0) |
readable(...) | $state + $effect |
derived(a, $a => $a * 2) | $derived(a * 2) |
get(store) | Direct access (no .get()) |
store.subscribe(fn) | $effect(() => { fn(value) }) |
Before:
import { writable, derived } from "svelte/store"
export const count = writable(0)
export const doubled = derived(count, ($c) => $c * 2)
// Usage
import { get } from "svelte/store"
get(count) // read
$count // auto-subscribeAfter:
export const count = $state(0)
export const doubled = $derived(count * 2)
// Usage
count // direct read (no $ prefix needed)🎨 Component Patterns
Polymorphic Components
<script>
interface Props {
href?: string
variant?: 'primary' | 'secondary'
children: import('svelte').Snippet
}
let { href, variant = 'primary', children }: Props = $props()
let Element = $derived(href ? 'a' : 'button')
</script>
<svelte:element this={Element} {href} class="btn {variant}">
{@render children()}
</svelte:element>Snippets (Slots 2.0)
<!-- Card.svelte -->
<script>
interface Props {
title?: string
children: import('svelte').Snippet
actions?: import('svelte').Snippet
}
let { title, children, actions }: Props = $props()
</script>
<div class="card">
{#if title}
<h3>{title}</h3>
{/if}
<div class="content">
{@render children()}
</div>
{#if actions}
<div class="actions">
{@render actions()}
</div>
{/if}
</div>
<!-- Usage -->
<Card title="Hello">
<p>Content here</p>
{#snippet actions()}
<button>Action</button>
{/snippet}
</Card>⚡ Performance Tips
- Use $derived for computations — caches results
- Avoid $effect for derived data — causes extra renders
- Pass state, not stores — simpler, no subscription overhead
- Use $effect.pre for DOM reads — before paint
- Cleanup in $effect — return cleanup function
<script>
// ✅ Good — derived for computation
let fullName = $derived(`${firstName} ${lastName}`)
// ❌ Bad — effect for derived data
let fullName = $state('')
$effect(() => { fullName = `${firstName} ${lastName}` })
</script>📝 TypeScript Tips
// Typed state
let user = $state<User | null>(null)
// Typed props
interface Props {
items: string[]
onSelect: (item: string) => void
}
let { items, onSelect }: Props = $props()
// Typed derived
let validItems = $derived<string[]>(items.filter((i) => i.length > 0))