Svelte Reactivity Explained: How Your UI Updates Automatically



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 whenever count 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:

  1. <script> → holds your JavaScript (variables, imports, logic).
  2. Markup (HTML) → your UI structure.
  3. <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 and squared 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 and count → 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 and name 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 reassigning total 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

  1. Open the page in your browser — you’ll see the input prefilled with octocat.
  2. Click Fetch User. → You should see Octocat’s name and avatar pop up underneath. 🎉
  3. Now type another GitHub username (for example, torvalds or octo) into the box.
  4. 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 like on: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 tracks username, 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, and taxRate are tracked.
    • Adding items updates the array reactively.
  • \$derived

    • itemCount, subtotal, tax, and total stay in sync with items and taxRate.
    • 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.
  • Conditional rendering

    • {#if itemCount === 0} shows “Cart is empty” until you add something.
  • Events

    • on:input and on:click wire up the UI to state updates.

🧪 Your Turn: Experiment!

Try modifying this cart:

  1. Change the hardcoded price: 10 to let users type a price.
  2. Add a “Remove” button next to each item (hint: items.splice(index, 1)).
  3. Change the taxRate value and watch tax + 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