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:
<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:
<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:
<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:
<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
<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 * quantitybut then never actually readingtotalanywhere the compiler can see reactively — the reactive statement still runs, but nothing depending onprice/quantityrenders 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.