Svelte 5 Tips and Tricks: Runes, Snippets, and More - SvGrid blog illustration

Svelte 5 Tips and Tricks: Runes, Snippets, and More

A practical, growing list of Svelte 5 tips - $state, $derived, snippets, $bindable and the runes patterns that replace stores and slots, each with a code snippet.

Svelte 5 rethinks reactivity around runes. If you are coming from Svelte 4, a handful of new patterns replace stores, slots, and the $: label. Here are the ones that come up every day, each with a short, copy-ready example. This page grows over time, so bookmark it.

$state makes any variable reactive

$state replaces the old top-level let reactivity. It works in components and, crucially, in plain .svelte.js / .svelte.ts modules too, so shared reactive state no longer needs a store.

let count = $state(0)
count++ // every reader re-runs

$derived instead of reactive $:

$derived(expr) replaces $: value = expr. For multi-statement logic use $derived.by(() => { ... }) and return the result.

let doubled = $derived(count * 2)
let total = $derived.by(() => {
  return items.reduce((s, i) => s + i.qty, 0)
})

$props() with defaults and rest

One rune replaces export let. Defaults, aliasing reserved words, and gathering the rest for spreading onto an element all work with plain destructuring.

let { size = 'md', class: cls, ...rest } = $props()

$bindable() for two-way props

Binding is no longer implicit. A child declares which props are bindable, so the two-way contract is visible in the code.

let { value = $bindable('') } = $props()
// parent: <Field bind:value={name} />

$effect can return its own cleanup

$effect tracks whatever it reads and re-runs when those change. The returned teardown makes subscriptions leak-free without a separate onDestroy.

$effect(() => {
  const id = setInterval(tick, 1000)
  return () => clearInterval(id)
})

$state.raw for large, replace-only data

Deep proxying every row of a 10k-item array is wasteful if you only ever replace the array. $state.raw gives shallow reactivity: mutations are not tracked, reassignment is.

let rows = $state.raw([])
rows = await fetchRows() // triggers; rows.push() would not

Snippets replace slots

Snippets are first-class values: name them, parameterize them, pass them to child components as props. One primitive covers default slots, named slots, and slot props.

{#snippet row(item)}
  <td>{item.name}</td>
{/snippet}
{@render row(user)}

Event handlers are just attributes now

Because handlers are attributes, {...props} forwards them automatically and there is no separate event-forwarding syntax to remember.

<button onclick={() => count++}>+1</button>

$inspect is console.log for runes

Add $inspect(x) to trace re-renders, or $inspect(x).with(fn) for custom handling. It only runs in dev, so you can leave it while iterating.

$inspect(count, filters)

Put shared state in a .svelte.js module

Rename the file with a .svelte.js / .svelte.ts extension so the compiler processes the runes, then import the state anywhere. This is the modern replacement for writable stores in most cases.

// counter.svelte.js
export const counter = $state({ n: 0 })

$state.snapshot for a plain copy

A $state value is a Proxy. Some APIs choke on that. $state.snapshot(x) returns a static clone with the proxies stripped, safe to serialize or hand off.

const payload = $state.snapshot(form)
await fetch('/api/save', { method: 'POST', body: JSON.stringify(payload) })

untrack() reads without subscribing

Handy when an effect should fire on one signal but only sample another. Everything read outside untrack is still tracked as normal.

import { untrack } from 'svelte'
$effect(() => { save(doc); const at = untrack(() => savedAt) })

$effect.pre runs before the DOM updates

Read scroll position or measurements in $effect.pre, decide what to do, then let the DOM update. The classic auto-scroll-on-new-message pattern.

$effect.pre(() => {
  messages.length // track new messages
  if (atBottom) tick().then(scrollToBottom)
})

Runes work as class fields

Class instances become reactive view-models: $state fields are tracked, $derived fields recompute, and methods mutate them. Great for carts, editors, wizards.

class Cart {
  items = $state([])
  total = $derived(this.items.reduce((s, i) => s + i.price, 0))
  add(p) { this.items.push(p) }
}

Built something with these?

These patterns power SvGrid, the Svelte 5-native data grid. If you work with tables, it is worth a look - the same runes you use above drive it under the hood.

Tagged: Svelte 5, Svelte runes, Snippets, Tutorials