Svelte Reactivity — How State Updates the UI
Build a real mental model of Svelte 4 reactivity: why reassignment drives updates, how the $: reactive statement derives values and runs side effects, how a writable store shares state, and how bind:value connects inputs.
Introduction
In Svelte, reactivity is the rule that ties a component's state to what the screen shows: when a value changes, the parts of the UI that read it update on their own. What makes Svelte 4 distinctive is how simple that looks. A top-level let count = 0 in a component is already reactive — there is no hook to call and no wrapper to reach for. The compiler watches which values your markup reads and rewrites your code so that changing one of them updates the DOM. That is the whole idea, and everything else on this page is a consequence of it.
Why does this matter? Because it lets you write state as ordinary variables and still get an interface that stays in sync. You do not manage subscriptions by hand or diff a virtual tree; you change a variable and the view follows. The one rule you must internalise is the trigger: Svelte's reactivity is driven by assignment. Reassigning a variable is what tells the compiler something changed. Mutating an object in place — pushing to an array, setting a field — does not, unless you follow it with an assignment. Hold that distinction and the rest falls into place.
Lessons
1. Reactivity is driven by assignment
A Svelte component is compiled, not interpreted. When you write count = count + 1, the compiler has already noticed that your markup reads count, so it inserts the code that updates the DOM right after the assignment. This is why a plain variable is reactive with no ceremony: the reactive machinery is generated around every place you assign. Read this as a single loop you can predict — an event assigns a new value, the assignment marks the variable dirty, and Svelte re-renders the pieces that depend on it. The setter is the assignment itself.
<script>
let count = 0;
</script>
<!-- reassigning count updates the button text -->
<button on:click={() => count = count + 1}>
Clicked {count} times
</button>
The count++ shorthand works too, because it is an assignment underneath. The rule to remember is that the equals sign is the trigger: no assignment, no update.
2. Mutating without reassigning does not update
Here is the corollary that trips people up. If you mutate an object or array in place and never reassign the variable that holds it, Svelte has no assignment to hook onto, so the UI goes stale. Pushing to an array changes the array but not the binding, so the list you rendered never redraws. The fix is always the same: turn the mutation into an assignment. Reassign the variable to itself after mutating, or build a new value and assign that. The idiomatic form is items = [...items, next], which mutates nothing and assigns a fresh array.
<script>
let items = [];
function add(x) {
items.push(x); // mutates in place — UI does NOT update
items = items; // the reassignment is what triggers reactivity
}
</script>
3. The $: reactive statement derives values
When one value should always be computed from another, you do not want to recompute it by hand every time the source changes. Svelte's answer is the reactive statement, written with the label syntax $:. A derived value like $: doubled = count * 2 re-runs automatically whenever any value it reads changes — here, whenever count is reassigned. The compiler reads the statement, finds its dependencies, and re-evaluates it exactly when they update. This keeps a derived value consistent with its source by construction, so it can never drift out of step.
<script>
let count = 0;
$: doubled = count * 2;
$: parity = count % 2 === 0 ? 'even' : 'odd';
</script>
<button on:click={() => count++}>count is {count}</button>
<p>doubled is {doubled} and the count is {parity}</p>
4. Reactive statements also run side effects
A $: statement is not limited to assigning a value. Give it a block and it becomes a side effect that re-runs when its dependencies change — logging, syncing to storage, or reacting to a threshold. $: { ... } runs the block, and $: if (condition) { ... } runs conditionally. The dependency tracking is the same in every form: Svelte re-runs the statement whenever a value it references is reassigned. This is the escape hatch for the times a plain derived value is not enough and you genuinely need to *do* something when state changes.
<script>
let count = 0;
// side-effect form: re-runs whenever count changes
$: if (count > 10) {
console.log('count passed ten:', count);
}
</script>
5. Stores share state across components
Component let variables are private to one component. When two components need to read and change the same value, you reach for a store. A writable store, created with writable(initial) from svelte/store, is a small object with set, update and subscribe methods that any component can import. The convenience that makes stores feel native is the $store auto-subscription: prefix a store with $ inside a component and Svelte subscribes for you, reads its current value reactively, and unsubscribes when the component is destroyed — no manual subscription to manage.
<script>
import { writable } from 'svelte/store';
const count = writable(0);
</script>
<!-- $count reads the store reactively; update changes it -->
<button on:click={() => count.update((n) => n + 1)}>
count is {$count}
</button>
6. bind:value connects inputs to state
Reading state into the page is one direction; getting user input back out is the other. The bind:value directive wires an input's value to a variable in both directions at once: type in the field and the variable updates, change the variable and the field follows. <input bind:value={name} /> replaces the manual pairing of a value attribute and an event handler that other frameworks require. Because the binding is an assignment under the hood, everything else on this page still applies — a $: statement that reads name re-runs on every keystroke, and the whole model stays coherent.
<script>
let name = '';
</script>
<input bind:value={name} placeholder="Your name" />
<p>Hello {name || 'stranger'}</p>
Practice
Predict first, then run. Clicking the button reassigns count, so the label updates and the $: derived doubled recomputes; typing in the field flows through bind:value into name, so the greeting tracks every keystroke. This one component shows assignment-driven updates, a derived value, and a two-way binding working together — edit it live in the editor:
Next steps
You have the model — now go deeper. Read the $: reactive statement reference for the exact re-run rules, and the writable store reference for set, update and the $store shorthand. For the most common input task, follow how to bind an input with bind:value, and browse the runnable Svelte reactivity examples for copy-paste patterns. Coming from another framework? Compare React state. Every snippet here runs in the editor.
Frequently asked questions
What makes a variable reactive in Svelte 4?
let in a component that the markup reads. Svelte's compiler makes it reactive automatically, and reassigning it — count = count + 1 — is what triggers a UI update. There is no hook or wrapper to call.Why does pushing to an array not update the UI in Svelte?
items.push(x) mutates the array in place with no assignment to detect, so the view goes stale. Follow it with items = items, or use items = [...items, x], to trigger the update.What does the $: reactive statement do?
$: doubled = count * 2 keeps a derived value in sync with its source, and $: { ... } runs a side effect on each change. Svelte tracks the dependencies for you.When should I use a store instead of a let variable?
let is private to it. Inside a component, read a store reactively with the $store auto-subscription shorthand rather than subscribing by hand.