
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.
Related reading
- SvGrid UI Components Tips and Tricks: Svelte 5 Buttons, Inputs, Tree, Forms
- SvGrid Tips and Tricks: Get More from Your Svelte Data Grid
- SvGrid Studio Tips and Tricks: Build Svelte Data Apps, Fast
- Snippets vs Slots in Svelte 5
- Runes vs Stores in Svelte 5 - When to Use Which
Tagged: Svelte 5, Svelte runes, Snippets, Tutorials