Master Svelte 5 runes — $state for reactive values, mutation-friendly updates, $derived for computed values, $effect for side effects, element refs, and reusable logic in .svelte.ts files.
Why: props come from the parent and are read-only; state is owned by the component and changes over time. You write state as let count = $state(initialValue) — $state is a "rune", a compiler instruction that makes the variable reactive. Assigning to it re-renders whatever uses it — that is the entire Svelte update model. No setter functions needed.
<script lang="ts">
// step is a prop (passed in, read-only)
let { step }: { step: number } = $props()
// count is state (owned here, changes over time)
let count = $state(0)
</script>
<!-- Assigning to a $state variable re-renders what uses it -->
<button onclick={() => (count += step)}>Count: {count}</button>Why: $state makes objects and arrays deeply reactive — Svelte wraps them in a proxy, a thin layer that notices every change. So unlike React, mutating IS allowed: push into arrays, change fields on objects, and the screen updates. No spreading, no setters, no immutability rules.
<script lang="ts">
// Deeply reactive — mutating is allowed (unlike React)
let todos = $state<string[]>([])
let user = $state({ name: 'Ada', age: 36 })
</script>
<div>
<button onclick={() => todos.push('Todo #' + (todos.length + 1))}>
Add
</button>
<button onclick={() => user.age++}>{user.name} is {user.age}</button>
<p>{todos.join(', ')}</p>
</div>Why: a $derived is a value calculated from other state — it recalculates only when something it reads changes. Reach for it whenever one value can be derived from another instead of storing both; never assign to it yourself. Note: bind:value keeps the input and the variable in sync — more in the Forms lesson.
<script lang="ts">
let query = $state('')
const products = ['Laptop', 'Phone', 'Tablet']
// Recalculates only when query changes — never set it yourself
let filtered = $derived(
products.filter((p) => p.toLowerCase().includes(query.toLowerCase())),
)
</script>
<div>
<input bind:value={query} placeholder="Search…" />
<p>{filtered.length} result(s): {filtered.join(', ')}</p>
</div>Why: effects connect your component to things Svelte does not control — timers, subscriptions, browser APIs. $effect(fn) runs after the component renders and re-runs whenever state it READS changes; return a cleanup function to undo the effect — Svelte calls it before each re-run and when the component leaves the screen. Note: for deriving values, use $derived, not $effect.
<script lang="ts">
// null until the first tick — that's why the type is Date | null
let now = $state<Date | null>(null)
$effect(() => {
// Runs after render — set up the outside-world connection
const id = setInterval(() => (now = new Date()), 1000)
// Cleanup — runs on unmount and before each re-run
return () => clearInterval(id)
})
</script>
<p>{now ? now.toLocaleTimeString() : 'Loading…'}</p>Why: sometimes you need the real element on the page — to focus an input, scroll, or measure it. bind:this puts the element itself into a variable. Note: the element only exists after the component is on screen, so it is undefined at first — hence the ?. when using it.
<script lang="ts">
let inputEl = $state<HTMLInputElement>()
function focusInput() {
inputEl?.focus()
}
</script>
<div>
<!-- bind:this = the real <input> element, not a copy -->
<input bind:this={inputEl} placeholder="Search…" />
<button onclick={focusInput}>Focus input</button>
</div>Why: runes are not locked inside components — any file named something.svelte.ts can use $state and $derived, so reusable logic becomes an ordinary exported function. This is Svelte's answer to React custom hooks. Note: expose state through a getter so consumers always read the live value.
// src/lib/counter.svelte.ts — runes work in .svelte.ts files too
export function createCounter(initial = 0) {
let count = $state(initial)
return {
// A getter keeps the value live — destructuring would freeze it
get count() {
return count
},
increment() {
count += 1
},
reset() {
count = initial
},
}
}
// In any component:
// import { createCounter } from '$lib/counter.svelte'
// const counter = createCounter()
// <button onclick={counter.increment}>Count: {counter.count}</button>