Reactivity

Chapter 2
Reactivity with $state
Making a value reactive with a rune — and why a counter just works with plain assignment

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

<script> let count = $state(0); function increment() { count++; } </script> <p>Count: {count}</p> <button onclick={increment}>+1</button>

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.

Runes are compiler magic, not function calls
Although $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?

<!-- this does NOT update the UI in Svelte 5 --> <script> let count = 0; // plain variable, not reactive function increment() { count++; // changes the variable, but the UI never re-renders } </script>

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.

This is the big Svelte 3/4 → 5 change
In older Svelte (3/4), a plain 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

let user = $state({ name: 'Philip', age: 35 }); function birthday() { user.age++; // mutating a property works — it's deeply reactive } let todos = $state([]); function add(text) { todos.push({ text }); // even .push() triggers an update }

$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

<p>{count * 2}</p> <p>{name.toUpperCase()}</p> <p>{isActive ? 'On' : 'Off'}</p>

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.)

 ReactVueSvelte 5
CreateuseState(0)ref(0)$state(0)
Readcountcount.value (script)count
UpdatesetCount(count+1)count.value++count++
Object mutationForbidden (new copy)Allowed (proxy)Allowed (proxy)

Coding Challenges

Challenge 1

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 solution
Challenge 2

Build 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 solution
Challenge 3

Build 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 solution

Chapter 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 let is not reactive in Svelte 5 — only $state is tracked
  • $state objects/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)