The Essential Six
Svelte 5 introduced runes — special symbols that make reactivity explicit. The documentation lists over a dozen of them. The internet is full of tutorials covering every variation.
Here’s the truth: you need six.
Not six to start with. Not six for beginners. Six, period. You can build production applications with just these six runes. Everything else is either an optimization you don’t need yet or a niche feature you may never need.
Let’s go through them.
$state — Local Ownership
$state makes a value reactive by wrapping it and tracking changes. When the value changes, anything that uses it updates automatically.
<script>
let count = $state(0)
</script>
<button onclick={() => count++}>
Clicked {count} times
</button> $state works with any value type: strings, numbers, booleans, arrays, objects.
Svelte also tracks deep mutations. You can change properties on objects or items in arrays directly:
<script>
let todos = $state([{ text: 'Learn runes', done: false }])
function toggle(todo) {
todo.done = !todo.done // This just works
}
</script> No spread operators. No immutable update patterns. Just mutate directly.
$derived — Computed Values
Use $derived for values computed from other state. These values cannot be mutated independently — they always reflect their dependencies.
<script>
let todos = $state([
{ text: 'Learn runes', done: false },
{ text: 'Build something', done: false }
])
let remaining = $derived(todos.filter((t) => !t.done).length)
</script>
<p>{remaining} left to do</p> $derived automatically tracks dependencies. When todos changes, remaining updates.
$derived.by — Grouped Transformations
$derived.by lets you define derived values as functions, which is useful for more complex computations:
<script>
let count = $state(1)
let summary = $derived.by(() => ({
double: count * 2,
triple: count * 3
}))
</script>
<p>{summary.double}, {summary.triple}</p> Another example: calculating statistics from an array.
<script>
let items = $state([1, 2, 3, 4, 5])
let stats = $derived.by(() => {
const sum = items.reduce((a, b) => a + b, 0)
const avg = sum / items.length
return { sum, avg }
})
</script>
<p>Sum: {stats.sum}, Average: {stats.avg}</p> Rule of thumb:
If a value can be computed from other values, use
$derived. Don’t store it in$stateand try to keep it synchronized—that’s a bug waiting to happen.
$props — Receive Data from Parent
$props declares what data your component accepts from its parent.
No hidden dependencies - all inputs are visible and predictable.
<!-- Button.svelte -->
<script>
let { label, onclick, disabled = false } = $props()
</script>
<button {onclick} {disabled}>
{label}
</button> Use destructuring to pull out individual props. Default values work naturally.
From the parent:
<Button label="Save" onclick={save} />
<Button label="Cancel" onclick={cancel} disabled={saving} /> $bindable — Allow Two-Way Binding
<!-- TextInput.svelte -->
<script>
let { value = $bindable() } = $props()
</script>
<input bind:value /> $bindable marks a prop as two-way bindable. The parent can then use bind::
<script>
let name = $state('')
</script>
<TextInput bind:value={name} /><p>Hello, {name}</p> Use sparingly. Most props should flow one direction. Two-way binding makes sense for form inputs and little else.
$effect — Run Side Effects (Rarely)
<script>
let title = $state('My App')
$effect(() => {
document.title = title
})
</script>
<input bind:value={title} /> $effect runs code when its dependencies change. It’s for side effects: things that reach outside your component like DOM manipulation, analytics, or subscriptions.
Important: Most of the time, you don’t need $effect. If you’re using it to synchronize state, you probably want $derived instead. If you’re using it to fetch data, you probably want a load function.
Cleanup works by returning a function:
<script>
let enabled = $state(true)
$effect(() => {
if (!enabled) return
const id = setInterval(() => console.log('tick'), 1000)
return () => clearInterval(id) // Cleanup
})
</script> $inspect — Debug Reactivity
<script>
let count = $state(0)
$inspect(count) // Logs whenever count changes
</script> $inspect is console.log that understands reactivity. It re-runs whenever the value changes, showing you exactly when and why updates happen.
For custom handling:
<script>
let data = $state({ x: 1, y: 2 })
$inspect(data).with((type, value) => {
if (type === 'update') debugger
})
</script> Remove before production. It’s a development tool.
That’s It
Six runes:
| Rune | Purpose |
|---|---|
$state | Make a value reactive |
$derived | Compute from other values |
$props | Receive data from parent |
$bindable | Allow two-way binding |
$effect | Run side effects (rarely) |
$inspect | Debug reactivity |
Yes, Svelte has more. $state.raw for performance optimization. $effect.pre for running before DOM updates. $props.id for generating unique IDs. You can learn those when you need them.
But these six? These are enough to build anything. Production applications have been built with nothing more.
The goal isn’t to know every feature. The goal is to build things that work and stay simple. These six runes are all you need for that.
Next up: we’ll talk about how to think with these tools — why data flow matters more than lifecycle, and how that shift prevents entire categories of bugs.