Reactivity and Components

Plain let variables as reactive state and reactive statements with the $: syntax.

Reactivity from plain let variables

In Svelte, a top-level variable declared with let inside a component's <script> block is automatically reactive — no useState, no ref(), no wrapper object. Whenever you assign to that variable, Svelte's compiler has already generated the code needed to update anywhere it's used in the template:

HTML
<script>
  let count = 0;

  function increment() {
    count += 1;
  }
</script>

<button on:click={increment}>
  Clicked {count} {count === 1 ? "time" : "times"}
</button>

Click the button, count += 1 runs, and the text inside the <button> updates immediately — no explicit "tell the framework something changed" call anywhere. This works because the compiler statically analyzes the component's source and rewrites count += 1 into count = count + 1; plus whatever DOM update calls are needed — reactivity here is a compile-time transformation, not a runtime proxy or tracking system watching for changes.

The catch: reassignment, not mutation

Svelte's compiler can only see and instrument plain assignments (count = ..., count += ...). Mutating an array or object without reassigning it does not trigger an update:

HTML
<script>
  let items = ["Apple", "Banana"];

  function addBad() {
    items.push("Cherry");   // mutates the array, but Svelte doesn't see an assignment — no update
  }

  function addGood() {
    items = [...items, "Cherry"];   // reassignment — Svelte sees this and updates the DOM
  }
</script>

<ul>
  {#each items as item}
    <li>{item}</li>
  {/each}
</ul>

<button on:click={addGood}>Add Cherry</button>

This is the single most common Svelte gotcha for newcomers coming from plain JavaScript habits: array.push(...), array.splice(...), and direct property assignment on an object (user.name = "x", without reassigning user itself) are all silently invisible to Svelte's reactivity unless followed by (or replaced with) an actual reassignment of the variable itself.

Reactive statements: $:

A reactive statement, written with the $: label, re-runs automatically whenever any variable it references changes — Svelte's answer to Vue's computed()/watch() and, loosely, React's useEffect for derived values:

HTML
<script>
  let price = 10;
  let quantity = 2;

  $: total = price * quantity;   // recalculates whenever price or quantity changes
  $: console.log(`Total is now ${total}`); // reactive statements can be side effects too, not just derivations
</script>

<p>Total: ${total}</p>

<input type="number" bind:value={price} />
<input type="number" bind:value={quantity} />

$: total = price * quantity looks like a label on a statement because, syntactically, it is one — $: is standard JavaScript label syntax, repurposed by Svelte's compiler to mean "re-run this whenever any variable it depends on changes." The compiler determines those dependencies automatically by statically analyzing which variables the statement reads — there's no dependency array to maintain by hand, unlike React's useEffect.

You can group several statements under one $: block too:

HTML
<script>
  let width = 10;
  let height = 5;

  $: {
    console.log(`Dimensions changed: ${width}x${height}`);
    console.log(`Area: ${width * height}`);
  }
</script>

A complete example: a value updating the DOM automatically

HTML
<script>
  let celsius = 0;

  $: fahrenheit = (celsius * 9) / 5 + 32;
</script>

<label>
  Celsius:
  <input type="number" bind:value={celsius} />
</label>

<p>{celsius}°C is {fahrenheit}°F</p>

Typing into the input updates celsius (via bind:value, covered on the next page), which causes the $: fahrenheit = ... statement to re-run, which in turn updates the <p> displaying it — a full chain of automatic updates from one small reassignment, none of it wired up manually.

Common mistakes

  • Calling array.push()/array.splice() or mutating an object property directly, expecting the UI to update — Svelte only reacts to actual assignments to the top-level variable, not in-place mutation.
  • Writing $: total = price * quantity but then never actually reading total anywhere the compiler can see reactively — the reactive statement still runs, but nothing depending on price/quantity renders it, so make sure the derived value is actually used in the template.
  • Forgetting that $: dependencies are determined by static analysis of what the statement reads — assigning to a variable inside a reactive statement's body without it appearing as a direct reference can occasionally confuse the dependency detection in more complex cases.