Reactive state with $state

Svelte · Essentials · 11 min

State that the UI follows automatically is the core of every Svelte component.

In Svelte 5, reactivity is explicit through runes, compiler keywords that start with $. The first is `$state`:

let count = $state(0);

count is a normal variable: read it as count, update it with count++ or count = 5, and every place in the markup that uses it updates. No .value, no setter function.

When the value is an object or array, $state makes it deeply reactive by wrapping it in a Proxy, so mutations work too: todos.push(item), todos[0].done = true and user.name = 'Ada' all update the UI.

Things to know:

  • Plain let x = 0 without the rune is not reactive in Svelte 5 components: the UI won't follow reassignments.
  • Updates are applied in a batch after your code finishes; the DOM is not updated synchronously. await tick() waits for it.
  • Destructuring copies values: let { name } = user gives a non-reactive snapshot.
  • Classes can use runes in fields: class Todo { done = $state(false) }.
  • console.log of a state proxy shows a Proxy; use `$inspect(value)` (dev only) to log the value whenever it changes, or $state.snapshot(value) to get a plain copy.

Runes are only valid in .svelte files and .svelte.js/.svelte.ts modules, and only at the top level of a declaration, not inside arbitrary expressions.

Example

<script>
  let count = $state(0);
  let todos = $state([
    { text: 'learn runes', done: false },
  ]);
  let plain = 0;                     // NOT reactive

  function add() {
    todos.push({ text: `todo ${todos.length + 1}`, done: false });  // mutation works
  }
  $inspect(todos);                   // logs on every change (dev only)
</script>

<button onclick={() => count++}>clicks: {count}</button>
<button onclick={() => plain++}>plain: {plain}</button>   <!-- stays 0 on screen -->
<button onclick={add}>add todo</button>

{#each todos as todo}
  <label><input type="checkbox" bind:checked={todo.done} /> {todo.text}</label>
{/each}

Remember

  • let x = $state(initial) makes a reactive variable updated by plain assignment
  • Objects and arrays in $state are deeply reactive proxies; mutate them directly
  • Use $inspect or $state.snapshot to log state, not raw console.log of proxies

Watch out for

  • Declaring let count = 0 without $state and wondering why the screen never updates.

Check yourself

  1. `let items = $state([]);` and later `items.push('a')`. Does the UI update?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: svelte.dev/docs/svelte/$state