Skip to content

The $state Rune

$state creates a reactive value. Read it like a normal variable and reassign it like a normal variable — the compiler makes every read a dependency and every write a trigger.

<script>
let count = $state(0);
</script>
<button onclick={() => count++}>{count}</button>

There are no setters and no special update calls. count++ is a real reassignment, and because the compiler tracks that the button’s text reads count, that one text node updates.

When you pass an object or array to $state, Svelte wraps it in a proxy so that mutations — not just reassignments — are reactive. Pushing to an array or setting a nested property triggers updates.

<script>
let todos = $state([{ text: 'learn runes', done: false }]);
function toggle(todo) {
todo.done = !todo.done; // mutating a nested property is reactive
}
function add(text) {
todos.push({ text, done: false }); // mutating the array is reactive
}
</script>

This is a real difference from React, where you must produce new objects/arrays. In Svelte, todos.push(...) and todo.done = true just work — the proxy notices the mutation. The reactivity is deep: nested objects and arrays are proxied too.

flowchart LR
  write["mutate: todo.done = true"] --> proxy["$state proxy notices the change"]
  proxy --> update["update exactly the dependent DOM"]
A $state object is a reactive proxy

Deep proxying costs a little and isn’t always wanted — for large immutable data you only ever replace, use $state.raw. Raw state is reactive on reassignment but not on mutation.

<script>
let settings = $state.raw({ theme: 'dark', density: 'cozy' });
// ✅ Reassigning a whole new object IS reactive:
settings = { ...settings, theme: 'light' };
// ❌ Mutating is NOT tracked with raw state:
// settings.theme = 'light'; // no update
</script>

Use $state.raw for values you treat immutably (replace wholesale) or large structures where you don’t want the proxy overhead.

Because $state objects are proxies, passing one to code that expects a plain object (a console.log that shows a Proxy, structuredClone, a third-party library) can be surprising. $state.snapshot returns a plain, non-reactive copy.

<script>
let user = $state({ name: 'Ada', roles: ['admin'] });
function save() {
const plain = $state.snapshot(user); // a normal object, not a proxy
api.save(plain);
}
</script>
How do you update `$state` in Svelte 5?
Why is `todos.push(newTodo)` reactive when `todos` is `$state([])`?
When would you use `$state.raw`?
What does `$state.snapshot(user)` return?