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:

OldNew
onMount$effect with DOM check
beforeUpdate/afterUpdate$effect.pre / $effect
onDestroycleanup 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 PatternRune 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-subscribe

After:

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

  1. Use $derived for computations — caches results
  2. Avoid $effect for derived data — causes extra renders
  3. Pass state, not stores — simpler, no subscription overhead
  4. Use $effect.pre for DOM reads — before paint
  5. 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))

🔗 Resources