Reactivity
Chapter 1's interpolated values never changed. For a value that changes and updates the UI, Svelte 5 uses the $state rune — a compiler keyword (recognizable by the $ prefix) that marks a variable as reactive. This is Svelte's answer to React's useState, Vue's ref, and Angular's signal — but it reads the most like ordinary JavaScript of any of them.
$state — A Reactive Value
let count = $state(0) declares a reactive variable holding 0. The remarkable part: you read it as plain count and update it with plain assignment — count++ — and the UI updates automatically. No .value (Vue), no setter function (React's setCount), no .update() call (Angular). The $state rune tells the compiler "track this variable," and the compiler rewrites your ordinary count++ into the precise DOM update behind the scenes.
$state(0) looks like a function call, it isn't one you import — it's a rune, a special keyword the Svelte compiler recognizes. You never import { $state }; it's just available, like a language built-in. The $ prefix is reserved for runes, which is how you spot them at a glance.
Why Not a Plain let?
A plain let count = 0 increments fine in memory, but Svelte 5 doesn't treat it as reactive — nothing tells the compiler to wire it to the DOM, so the displayed number never changes. The same trap as a plain variable in React or Vue. The $state rune is the single thing that makes it reactive.
let count = 0 at the top of a component was automatically reactive — the compiler tracked all top-level let declarations. Svelte 5 made reactivity explicit via $state instead, because the old implicit magic didn't work outside components and had confusing edge cases. So if you see a reactive-looking plain let in a tutorial with no $state, it's pre-Svelte-5 code.
Reactive Objects and Arrays
$state makes objects and arrays deeply reactive — mutating a nested property (user.age++) or even calling .push() on an array triggers a UI update. This is a notable contrast with React, where you must create a new object/array (spread, map, filter) because React compares by reference and never mutates. Svelte lets you mutate directly, which often reads more naturally — under the hood it uses a proxy (like Vue's reactive) to detect those mutations.
Markup Expressions
Interpolation isn't limited to a bare value — any JavaScript expression works inside { }: arithmetic, method calls, ternaries. Identical to React's curly braces and Vue's interpolation: expressions only, no statements. (For a value reused in several places or with heavier logic, you'd reach for $derived — next chapter.)
| React | Vue | Svelte 5 | |
|---|---|---|---|
| Create | useState(0) | ref(0) | $state(0) |
| Read | count | count.value (script) | count |
| Update | setCount(count+1) | count.value++ | count++ |
| Object mutation | Forbidden (new copy) | Allowed (proxy) | Allowed (proxy) |
Coding Challenges
Build a counter with a $state value starting at 0 and a +1 button that increments it with plain count++, displaying the count via interpolation.
📄 View solutionBuild a component with a $state object holding firstName and lastName, plus a button that changes one property directly (e.g. user.firstName = '...'), displaying "Full name: [first] [last]" with both updating live.
📄 View solutionBuild a component with a $state array of strings and an "Add item" button that pushes a new string onto it directly, confirming the rendered list updates even though you used .push() rather than creating a new array.
📄 View solutionChapter 2 Quick Reference
- $state(value) — declares a reactive variable (=
useState/ref/signal) - Read it as a plain variable; update it with plain assignment (
count++) — no setter, no.value - A rune is a compiler keyword (
$prefix) — never imported, just available - A plain
letis not reactive in Svelte 5 — only$stateis tracked $stateobjects/arrays are deeply reactive — mutation (.push(),obj.x++) triggers updates- { } accepts any JS expression (math, method calls, ternaries)
- Next chapter: derived state (
$derived) and effects ($effect)