This content originally appeared on DEV Community and was authored by Ali Aslam
One of the biggest reasons developers fall in love with Svelte is its reactivity model.
Unlike frameworks like React (where you have to call setState
or use hooks), in Svelte you just… assign to a variable, and the UI updates. No ceremony. No boilerplate. Just magic.
By the end of this article, you’ll know:
- How Svelte tracks reactive state.
- What reactive declarations are.
- How auto-updating works (and when it doesn’t).
- Gotchas you should watch out for.
Let’s start simple.
Step 1: Your First Reactive Variable
Last time (in previous article), we built a counter and it “just worked.” But why? Why does Svelte magically know to update your UI when you increment a variable?
That’s the heart of Svelte’s reactivity model — and to really “get” it, we’re going to zoom in on this tiny example and unpack every moving part.
Here’s the minimal version again:
src/routes/+page.svelte
<script>
let count = $state(0);
</script>
<h1>Count: {count}</h1>
<button on:click={() => count++}>Increment</button>
Let’s break this down, line by line
1. Declaring reactive state
let count = $state(0);
At first glance, this looks like a regular variable assignment. But the $state
wrapper is your way of telling Svelte:
“Hey, compiler — track this value. Whenever it changes, make sure the UI is updated.”
Without $state
, count
would be just another local variable in your script. With $state
, it becomes reactive state, tied directly to your markup.
Pro tip: If you’re a JavaScript developer, you can stay in your comfort zone with let count = 0;. Today, all variables are tracked. But Svelte is nudging us toward $state to make reactive variables explicit, and that flexibility may go away in the future. It’s worth getting used to $state now.
2. Referencing in markup
<h1>Count: {count}</h1>
This is where the magic starts. The curly braces ({}
) tell Svelte:
“Insert the current value of
count
here. And if it ever changes, update this part of the DOM.”
Behind the scenes, the compiler doesn’t just “print the number once.” It generates code that reassigns the text whenever count
updates. That means your <h1>
stays in sync without you lifting a finger.
3. Updating the state
count++;
Here’s the shocking part: you didn’t call setCount
, or this.setState
, or dispatch
. You just incremented a variable like you would in vanilla JavaScript.
But because it’s a $state
variable, Svelte’s compiled code knows to re-render anything that depends on it. That’s reactivity in action.
Why this feels different
If you’ve used React, Vue, or Angular, you might be blinking at your screen right now. “Wait, that’s it?”
Yes, that’s it. No special function calls. No useState
. No dependency arrays. No magical “diffing algorithm” running at runtime.
Svelte takes care of it ahead of time during compilation. That’s why you can write code that feels like plain JavaScript but behaves like a reactive UI framework. Even $state is optional today — until it’s not.
Assignments Trigger Reactivity
Let’s stress this point again, because it’s the foundation of everything:
When you reassign count
, Svelte knows it changed and re-renders anything that depends on it ({count}
in this case).
That’s the entire idea of reactivity:
- Variables you declare in
<script>
become reactive when marked with$state
. - Any time you assign to them, the DOM updates automatically.
Why this feels different from React
If you’ve dabbled in React (or seen enough code samples online), you know how state updates look there:
// React
const [count, setCount] = useState(0);
<button onClick={() => setCount(count + 1)}>Increment</button>
Why the ceremony? Because React can’t just watch plain variables. Instead, it invented the virtual DOM.
The virtual DOM was clever: it kept a lightweight copy of your UI in memory, recalculated it whenever state changed, and then compared it with the real DOM to apply minimal updates. This solved a real problem in 2013: keeping UIs in sync with fast-changing data.
But… it came with runtime costs. React does the bookkeeping while your app is running.
Svelte takes a different route: it’s a compiler. At build time, it analyzes your code and generates direct DOM updates like:
countElement.textContent = count;
So instead of building and diffing trees in memory, your compiled app just updates the DOM directly. The result: smaller bundles, faster runtime, and predictable updates.
Key takeaway: In Svelte, plain variable assignments are reactive because the compiler turns them into optimized DOM updates. That’s why
count++
just works.
� Why React chose runtime (and Svelte can do compile-time)
You might wonder: if compile-time is so great, why didn’t React do that?
- React’s goal (2013) → be a drop-in UI library, no build tools required. Runtime logic made it portable.
- Tooling back then → modern compilers and bundlers (like Vite, Rollup, esbuild) didn’t exist or weren’t widely used. A compiler-first approach would’ve been too hard to adopt.
- The result → the virtual DOM was the best runtime solution for its era.
Fast-forward to today:
- Developers are used to build steps.
- Tooling is mature.
- That’s why Svelte can move the heavy lifting to compile-time and ditch the runtime tax.
Step 2: Derived Reactivity
So far, reactivity has been about: “the UI reacts when a variable changes.”
But what if another variable depends on that one?
That’s where derived reactivity comes in.
src/routes/+page.svelte
<script>
let count = $state(0);
let doubled = $derived(count * 2);
</script>
<h1>Count: {count}</h1>
<h2>Doubled: {doubled}</h2>
<button on:click={() => count++}>Increment</button>
Here’s what’s happening:
-
doubled
is automatically recalculated whenevercount
changes. - When you click the button, both
<h1>
and<h2>
update instantly. - No hooks, no dependency arrays, no
useMemo
. Just declare the relationship, and Svelte takes care of the rest.
This is a big mental shift. In other frameworks, you often need to tell the system “recalculate this when these variables change.” In Svelte, you just write the formula. The compiler figures out the dependencies.
Together, these two steps give you the foundation:
- Step 1 → state-driven reactivity (variables directly update the UI).
- Step 2 → derived reactivity (variables that depend on other variables).
This mental model scales up beautifully — from simple counters to complex apps.
A quick note on <script>
in Svelte
In every .svelte
file, you can have three main sections:
-
<script>
→ holds your JavaScript (variables, imports, logic). - Markup (HTML) → your UI structure.
-
<style>
→ component-scoped CSS.
Variables in <script>
that you mark as $state
are directly tied to your UI.
It’s just you, JavaScript, and some very helpful magic.
Step 3: Multiple Derived Values
Sometimes you don’t just want to track state — you want to compute new values from it. For example, given a count
, you might want to display both its double and its square.
That’s where $derived
comes in.
src/routes/+page.svelte
<script>
let count = $state(2);
let doubled = $derived(count * 2);
let squared = $derived(count * count);
</script>
<h1>Count: {count}</h1>
<h2>Doubled: {doubled}</h2>
<h2>Squared: {squared}</h2>
<button on:click={() => count++}>Increment</button>
What $derived
really means
Think of $derived
as a live formula. You’re telling Svelte:
“This value always equals the result of this expression. Whenever its inputs change, update it.”
So here:
- When
count
changes,doubled
andsquared
recalculate automatically. - No extra function calls. No worrying about stale values.
Why not just write {count * 2}
inline?
You could!
<h2>Doubled: {count * 2}</h2>
That works fine for one-off expressions. But $derived
shines when:
- You need to reuse the computed value in multiple places (e.g. both in markup and in a script).
- The computation isn’t trivial and repeating it everywhere would be messy.
- You want to build on it further (hint: that’s Step 6).
Takeaway:
$derived
gives you clean, reusable, always-up-to-date values. It’s a way of saying “don’t just track state — also track relationships between state.”. This concept is also known as declarative reactivity — you declare relationships, and Svelte figures out when to update them.
Quick check-in: Are you typing out the code and trying it out?
Or at the very least copy-pasting it into your project?
If not, I’d highly encourage you to do it. Seeing the UI update while you tweak values will make these concepts click way faster than just reading about them.
Step 4: Arrays and Reactivity
Here’s where many beginners hit their first “Wait, why didn’t that update?” moment.
Try this:
src/routes/+page.svelte
<script>
let numbers = [1, 2, 3];
function addNumber() {
numbers.push(numbers.length + 1);
}
</script>
<h1>Numbers: {numbers.join(', ')}</h1>
<button on:click={addNumber}>Add Number</button>
Click the button… nothing happens.
Why didn’t it update?
This comes down to how Svelte’s reactivity works:
- Assignments trigger updates.
- Mutations do not.
When you do:
numbers.push(4);
…the array is modified in place. The variable numbers
itself didn’t change — it still points to the same array. Svelte’s compiler is watching for assignments like:
numbers = [...numbers, 4];
So from its perspective, no update happened.
This reflects a principle you may have heard of: immutability. In many reactive frameworks, instead of mutating arrays/objects directly, you create a new array/object with the updated value. That’s what Svelte expects if you’re using plain variables.
The Fix: Reassign the Array
Instead of mutating, you give numbers
a brand-new value:
function addNumber() {
numbers = [...numbers, numbers.length + 1];
}
What’s with the …?
That’s the spread operator in JavaScript. It’s like saying:
“Take everything inside this array, and unpack it into a new one.”
So if numbers = [1, 2, 3], then:
[…numbers, 4]
becomes:
[1, 2, 3, 4]
You end up with a brand-new array that contains all the old values plus the new one.
This matters because now numbers has been reassigned to a fresh value (at a new memory location) — and that triggers Svelte’s reactivity.
The Better Way: Use $state
Of course, constantly cloning arrays feels clunky. This is where $state
comes in handy:
src/routes/+page.svelte
<script>
let numbers = $state([1, 2, 3]);
function addNumber() {
numbers.push(numbers.length + 1); // mutation is tracked
}
</script>
<h1>Numbers: {numbers.join(', ')}</h1>
<button on:click={addNumber}>Add Number</button>
Click the button → it works
Because $state
tells Svelte to fully track the variable, even mutations like push
, pop
, or splice
will update the DOM.
Takeaway:
- With plain arrays, Svelte only reacts to reassignments — think in terms of immutability.
- With
$state
, mutations are tracked too, so arrays behave naturally.
Step 5: Reactivity with Objects
Arrays aren’t the only data structures we need in our apps — objects show up everywhere too. And just like arrays, objects have their own “gotcha” moments when it comes to reactivity.
Let’s start with a basic example:
src/routes/+page.svelte
<script>
let user = $state({ name: "Alice", age: 30 });
function incrementAge() {
user.age += 1;
}
</script>
<p>{user.name} is {user.age} years old.</p>
<button on:click={incrementAge}>Happy Birthday!</button>
Now click the button. You’ll see age of Alice go up immediately .
As we saw earlier with array example, mutations to $state
objects are also automatically tracked. You don’t need to reassign a copy of the object every time. In older patterns, you might have had to do something like:
user = { ...user, age: user.age + 1 };
That’s no longer necessary when the object is wrapped in $state
. Just update it directly, and the DOM reflects the change.
Key takeaway: When your state is wrapped in
$state
, both arrays and objects are fully reactive — mutations are tracked, no extra ceremony required.
Step 6: Chained Reactivity
Earlier in Step 3, we saw how you can branch out from one piece of state into multiple derived values (like doubled
and squared
). That was parallel derivation — multiple values depending directly on the same source.
But what if one derived value should depend on another? That’s chained reactivity.
src/routes/+page.svelte
<script>
let price = $state(100);
// tax depends on price
let tax = $derived(price * 0.1);
// total depends on both price and tax
let total = $derived(price + tax);
</script>
<h1>Price: ${price}</h1>
<h2>Tax: ${tax}</h2>
<h2>Total: ${total}</h2>
<button on:click={() => price += 10}>Increase Price</button>
How it flows
- When
price
changes →tax
recalculates. - When
tax
changes →total
recalculates. - The DOM stays perfectly in sync.
And you didn’t wire anything manually. Because you declared the relationships with $derived
, Svelte’s compiler knows the dependency graph:
price → tax → total
Contrast with Step 3
-
Step 3 (multiple derived values): one source → multiple independent branches (
count → doubled
andcount → squared
). -
Step 6 (chained reactivity): one source → values feeding into each other (
price → tax → total
).
Both are powerful patterns, and together they cover most situations where one piece of data drives many others.
Takeaway:
$derived
doesn’t just save you from repeating calculations — it gives you a declarative way to model data dependencies. Whether branching or chaining, the compiler takes care of the wiring so you can focus on the logic.
Step 7: Reactive Blocks
So far, our reactive code has mostly been about assignments: “this value depends on that one.” But sometimes you want to run a block of code whenever certain variables change. That’s where $effect
comes in.
src/routes/+page.svelte
<script>
let count = $state(0);
let name = $state("Alice");
$effect(() => {
console.log(`Count is ${count}, name is ${name}`);
if (count > 5) {
alert("Whoa! Count is big now 🚀");
}
});
</script>
<button on:click={() => count++}>Increment</button>
<button on:click={() => name = "Bob"}>Change Name</button>
How it works
- Svelte looks at all variables used inside the effect (
count
andname
here). - Whenever any of them changes, the whole block re-runs.
- You don’t have to list dependencies yourself — Svelte figures it out.
So if you click Increment, you’ll see new logs (and maybe an alert). If you click Change Name, you’ll also see logs. Both variables drive the same block.
Why console logs instead of UI updates?
Good catch: the UI here looks super bare. That’s on purpose. $effect
is not usually for rendering things in the DOM — that’s what normal markup and $state
/$derived
variables are for.
Instead, $effect
is meant for side effects:
- writing to the console (for debugging),
- showing an alert,
- syncing to localStorage,
- kicking off a network request,
- starting or stopping an animation.
So here we log to the console just so you can see the reactivity happening. In a real app, you’d use $effect
for those “do something extra when state changes” situations.
If you’ve never used
console.log
before: right-click your page, choose Inspect, and open the Console tab. That’s where the messages will appear.
Takeaway: Use
$effect
when you need to react to changes with side effects — not for normal UI updates.
Gotcha: Don’t overload reactive blocks
Reactive blocks ($effect
) are incredibly useful, but they re-run every time a dependency changes. That means you should avoid doing heavy work (like big loops or API calls) directly inside them.
Instead:
- Use reactive blocks for lightweight side effects (logging, triggering an animation, simple checks).
- For heavier tasks, call a helper function from inside the block.
This keeps your reactivity system lean and avoids slowing down the UI.
With this one example, you see both:
- a single dependency (
count
driving the alert), and - multiple dependencies (
count
+name
in the console).
Reactive blocks let you express logic declaratively without worrying about wiring dependencies.
Step 8: Derived Values (a.k.a Computed Values)
So far, we’ve been dealing with plain state — variables you update directly. But what about values that should always be calculated from other values?
That’s where derived values come in.
The Problem: Calculated Once, Then Stale
Let’s try building a little shopping scenario:
src/routes/+page.svelte
<script>
let price = $state(20);
let quantity = $state(3);
// ❌ Only calculated once, not reactive
let total = price * quantity;
</script>
<p>Price: ${price}</p>
<p>Quantity: {quantity}</p>
<p>Total: ${total}</p>
<button on:click={() => price += 5}>Increase Price</button>
<button on:click={() => quantity++}>Increase Quantity</button>
Go ahead and click the buttons. price
and quantity
change as expected… but total
stays frozen at the original calculation.
Why? Because total = price * quantity
ran only once when the component loaded. It’s just a plain JavaScript assignment — Svelte isn’t tracking it.
The Fix: $derived
To keep total
in sync automatically, declare it as a derived value:
src/routes/+page.svelte
<script>
let price = $state(20);
let quantity = $state(3);
// ✅ Always recalculates when price or quantity changes
let total = $derived(price * quantity);
</script>
<p>Price: ${price}</p>
<p>Quantity: {quantity}</p>
<p>Total: ${total}</p>
<button on:click={() => price += 5}>Increase Price</button>
<button on:click={() => quantity++}>Increase Quantity</button>
Now click the buttons again: total
updates instantly. No extra code, no manual recalculation — just a clear declaration of intent.
Mental Model: Think Spreadsheets
If you’ve ever used Excel or Google Sheets, this will feel familiar:
A1 = 20
A2 = 3
A3 = A1 * A2
- If you change A1 or A2, A3 updates automatically.
- You don’t press a “recalculate” button — the dependency is built in.
Svelte does the same with $derived
. It builds a dependency graph during compilation and keeps everything fresh.
Counter vs. Derived: Two Flavors of Reactivity
You might wonder: “But plain count++
worked earlier, so why not total
?”
Here’s the difference:
- With
count
, you’re directly reassigning the variable (count = count + 1
) on button click. That reassignment is what Svelte’s compiler watches for, so the UI updates. - With
let total = price * quantity;
, you’re not reassigningtotal
after the first run. It’s just a one-time calculation that never changes.
That’s where $derived
steps in. It tells Svelte:
“Don’t treat this as a one-off assignment, treat it like a formula that depends on other state. Keep it up to date for me.”
And this is the bigger lesson: explicit markers like
$state
and $derived
are for your convenience, not Svelte’s.
- Svelte could often “guess” which variables are reactive — and in fact, it still does if you forget.
- But when you use
$state
or$derived
, you’re making your intent obvious to future-you (and your teammates). No hidden magic, no guessing games.
It’s like putting a bright sticky note on your code saying:
- “This variable changes over time.” (
$state
) - “This variable is a formula, not a snapshot.” (
$derived
)
That clarity becomes priceless as your app grows.
Gotcha: Circular Dependencies
One important rule: keep your dependencies flowing in one direction. If you try to make them loop back on each other, you’ll end up in an infinite loop.
Bad idea:
<script>
let a = $derived(b + 1);
let b = $derived(a + 1); // 🚨 Infinite loop!
</script>
As long as data flows downstream — no circles — you’re safe.
Bonus: Chaining Derived Values
Back in Step 6, we looked at price → tax → total
. That’s actually just chained derived values. Each one depends on the previous, and Svelte keeps the whole chain in sync without you lifting a finger.
Takeaway:
$derived
is your tool for values that should always reflect other values. It makes your code cleaner, avoids stale state, and turns your app into a living spreadsheet.
Step 9: Async Reactivity
So far, our reactivity examples have been instant: change a number, see the DOM update right away. But real apps often deal with data that takes time to arrive — maybe from a server or an API.
The great news: Svelte’s reactivity works just as smoothly with asynchronous code. Let’s see how.
First try: fetch on button click
Let’s make a page that looks up GitHub user profiles. We’ll start the old-fashioned way: click a button to fetch.
src/routes/+page.svelte
<script>
let username = $state("octocat");
let userData = $state(null);
async function loadUser() {
const res = await fetch(`https://api.github.com/users/${username}`);
userData = await res.json();
}
</script>
<input
value={username}
placeholder="GitHub username"
on:input={(e) => { username = e.target.value; }}
/>
<button on:click={loadUser}>Fetch User</button>
{#if userData}
<h2>{userData.name}</h2>
<img src={userData.avatar_url} width="100" />
{/if}
Try it yourself
- Open the page in your browser — you’ll see the input prefilled with
octocat
. - Click Fetch User.
→ You should see Octocat’s name and avatar pop up underneath.
- Now type another GitHub username (for example,
torvalds
orocto
) into the box. - Press the button again to fetch that profile.
This gives you instant feedback: type, click, see results.
What’s happening here:
- Typing in the input updates the
username
variable (on:input
is just likeon:click
, but for text fields). - Clicking the button calls
loadUser()
, which fetches JSON from GitHub. - When we assign the result to
userData
, the{#if userData}
block becomes true and shows the profile info.
{#if …}
is Svelte’s way of conditionally rendering content. If the expression is truthy, the block shows up; otherwise it stays hidden.
The reactive way: automatic fetching
The button works, but we can go one step further. Instead of saying “fetch only when I click,” why not just say “fetch whenever username
changes”? That’s what $effect
is for.
src/routes/+page.svelte
<script>
let username = $state("octocat");
let userData = $state(null);
$effect(() => {
if (!username) {
userData = null;
return;
}
fetch(`https://api.github.com/users/${username}`)
.then(r => r.json())
.then(data => { userData = data; });
});
</script>
<input
placeholder="GitHub username"
on:input={(e) => { username = e.target.value; }}
/>
{#if userData}
<h2>{userData.name}</h2>
<img src={userData.avatar_url} width="100" />
{/if}
Here’s the magic:
-
$effect
automatically tracksusername
, since we use it inside. - Each time you type,
username
updates → the effect reruns → a new fetch is made. - When the response comes back, assigning to
userData
triggers the DOM to update.
You don’t have to list dependencies manually. The compiler does the wiring.
A little warning: race conditions
Async work can overlap. If you type octo
(slow response) and quickly change to octocat
(fast response), the slower one might finish later and overwrite the newer data. (Do try it out and see for yourself)
For now, don’t worry — just be aware this can happen. Later we’ll explore solutions like AbortController
, debouncing, or SvelteKit’s load
function.
Takeaway: Async reactivity works the same as everything else you’ve learned. Assign to a reactive variable — whether instantly or after a fetch — and the UI reacts.
Step 10: Common Gotchas
By now you’ve seen how smooth reactivity feels in Svelte. But every magic trick has a few “hidden wires” — little details that can trip you up if you don’t know they’re there. Let’s walk through the most common mistakes beginners run into (and how to dodge them).
1. Forgetting $state
The #1 pitfall: declaring state as a plain variable and forgetting to mark it as tracked.
<script>
let count = 0; // ❌ plain variable, not explicitly tracked
function increment() {
count++;
}
</script>
<h1>{count}</h1>
<button on:click={increment}>Increment</button>
This often still works today — Svelte tries to track plain variables — but it’s not the recommended approach.
When you write:
let count = $state(0);
…you’re telling Svelte, “this variable belongs to the reactive system.” It removes ambiguity and makes it clear to future-you (and teammates) that this drives the UI.
Bottom line:
let count = 0;
works for now, but $state
is the safe, future-proof choice.
2. Thinking mutations won’t update the UI
If you’ve skimmed older tutorials, you might have read:
“Svelte only reacts to reassignments, not mutations.”
That used to be true. But with $state
, mutations are tracked.
Example with an array:
<script>
let numbers = $state([1, 2, 3]);
function addNumber() {
numbers.push(numbers.length + 1); // ✅ tracked automatically
}
</script>
<h1>{numbers.join(', ')}</h1>
<button on:click={addNumber}>Add</button>
And with an object:
<script>
let user = $state({ name: "Alice", age: 30 });
function birthday() {
user.age++; // ✅ updates the UI
}
</script>
<p>{user.name} is {user.age} years old.</p>
<button on:click={birthday}>Happy Birthday!</button>
Both update instantly. No more cloning arrays or spreading objects just to trigger updates.
Takeaway:
$state
makes mutations reactive. Forget $state
, and you’re back in the old world where only reassignments count.
3. Overstuffing $effect
blocks
It’s tempting to throw everything into one $effect
:
<script>
let a = $state(1);
let b = $state(2);
let c = $state(3);
$effect(() => {
console.log(a * 2);
console.log(b + 3);
console.log(c + 4);
});
</script>
This works, but it’s messy — you lose track of what depends on what.
A cleaner way is to use $derived
for computed values:
<script>
let a = $state(1);
let b = $state(2);
let c = $state(3);
let doubled = $derived(a * 2);
let sum = $derived(b + 3);
let shifted = $derived(c + 4);
</script>
Now each relationship is self-contained and obvious.
Guideline:
- Use
$effect
for side effects (logging, fetching, imperative work). - Use
$derived
for calculations.
4. Expecting batching like in React
In React, state updates are batched and applied later. In Svelte, reactivity is synchronous: every assignment immediately re-runs the reactive graph.
<script>
let count = $state(0);
$effect(() => {
console.log("Count is", count);
});
function incrementTwice() {
count++;
count++;
}
</script>
<h1>{count}</h1>
<button on:click={incrementTwice}>Increment Twice</button>
Click the button → console shows:
Count is 1
Count is 2
Each count++
triggers reactivity right away. Nothing is batched or deferred.
That might feel unusual if you’re coming from React, but it keeps Svelte’s model simple and predictable: assignment = update, immediately.
Step 11: Hands-On Recap
Shopping Cart Edition
We’ve seen $state
, $derived
, $effect
, and conditional rendering in isolation. Now let’s combine them into a mini shopping cart app.
src/routes/+page.svelte
<script>
// State: tracked variables
let items = $state([]);
let newItem = $state("");
let taxRate = $state(0.1); // 10%
// Derived values: formulas that stay in sync
let itemCount = $derived(items.length);
let subtotal = $derived(items.reduce((sum, item) => sum + item.price, 0));
let tax = $derived(subtotal * taxRate);
let total = $derived(subtotal + tax);
// Add a new item (hardcoded price for demo)
function addItem() {
if (!newItem.trim()) return;
items.push({ name: newItem, price: 10 });
newItem = "";
}
// Side effect: alert if cart gets big
$effect(() => {
if (itemCount > 5) {
alert("Whoa, that’s a big cart! 🛒");
}
});
</script>
<h1>My Cart</h1>
<input
placeholder="Add item"
value={newItem}
on:input={(e) => (newItem = e.target.value)}
/>
<button on:click={addItem}>Add</button>
{#if itemCount === 0}
<p>Your cart is empty. 🛍</p>
{:else}
<ul>
{#each items as item}
<li>{item.name} - ${item.price}</li>
{/each}
</ul>
<p>Items: {itemCount}</p>
<p>Subtotal: ${subtotal}</p>
<p>Tax: ${tax.toFixed(2)}</p>
<h2>Total: ${total.toFixed(2)}</h2>
{/if}
What’s happening here?
This little app touches almost everything we’ve learned:
-
\$state
-
items
,newItem
, andtaxRate
are tracked. - Adding items updates the array reactively.
-
-
\$derived
-
itemCount
,subtotal
,tax
, andtotal
stay in sync withitems
andtaxRate
. - No manual recalculation needed — change one input, everything flows.
-
-
\$effect
- Whenever
itemCount
changes, the effect runs. If the cart grows too large, you get a playful alert.
- Whenever
-
Conditional rendering
-
{#if itemCount === 0}
shows “Cart is empty” until you add something.
-
-
Events
-
on:input
andon:click
wire up the UI to state updates.
-
Your Turn: Experiment!
Try modifying this cart:
- Change the hardcoded
price: 10
to let users type a price. - Add a “Remove” button next to each item (hint:
items.splice(index, 1)
). - Change the
taxRate
value and watchtax
+total
recalc automatically.
Every tweak reinforces the same truth: Svelte’s reactivity is just variables + assignments. The compiler does the heavy lifting.
Final Thoughts
You’ve just completed a full tour of Svelte’s reactivity system — the beating heart of how apps stay in sync with your data. From the humble $state
counter, all the way to arrays, derived formulas, async fetches, and even a mini shopping cart app, you’ve seen how far you can get with nothing more than plain assignments and clear intent.
No extra libraries. No boilerplate. No “magic incantations.” Just variables → DOM.
Here’s the mental toolkit you now carry with you:
-
$state
→ mark the variables that drive your UI. -
$derived
→ write formulas once, let them stay in sync forever. -
$effect
→ handle side effects like logging, fetching, or alerts. -
{#if}
,{#each}
→ let the DOM adapt automatically as your data changes.
Where We’re Headed Next
This was just the foundation. In the next article, we’ll explore how reactivity flows across components:
- Passing data down with props.
- Composing components together.
If reactivity is the heartbeat of a Svelte app, components are the organs — each with its own job, working together to bring your app to life.
So take a breather, celebrate what you’ve built today , and when you’re ready, let’s continue the journey.
Next (Coming up): Svelte Components Explained: Props & Composition Made Simple.
This content originally appeared on DEV Community and was authored by Ali Aslam