Mastering Svelte 5 Runes: The Ultimate Guide to Modern Reactivity

For years, Svelte has been the “darling” of the frontend world, praised for its simplicity and its revolutionary “no-virtual-DOM” approach. However, as web applications grew more complex, the limitations of Svelte’s original reactivity model—based on the let keyword and the somewhat cryptic $: label—started to show. Developers often struggled with deeply nested reactivity, cross-file state sharing, and the nuances of how the compiler tracked dependencies.

Enter Svelte 5 and its headline feature: Runes. This isn’t just a minor update; it is a fundamental shift in how Svelte handles data. By moving from compiler-time heuristics to a signal-based system, Svelte 5 offers more power, better performance, and a much more predictable development experience. Whether you are a beginner looking to start with the latest tech or an intermediate developer migrating from Svelte 4, this guide will provide a deep, comprehensive dive into the world of Runes.

In this post, we will explore why Runes matter, how to use them effectively, and how to avoid the common pitfalls that even seasoned developers encounter. By the end, you will be equipped to build enterprise-grade applications using the most modern reactivity patterns available today.

Why Svelte 5 Runes? Understanding the Shift

To understand why Svelte 5 introduced Runes, we first need to look at the “Old Way.” In Svelte 3 and 4, reactivity was tied to the component’s top-level scope. If you wanted a variable to be reactive, you simply declared it with let. If you wanted to derive a value, you used the $: syntax.

While elegant for small components, this approach had three major flaws:

  • Refactoring Difficulty: You couldn’t easily move reactive logic out of a .svelte file into a .js or .ts file without using Svelte Stores.
  • Inconsistent Reactivity: Sometimes Svelte’s compiler couldn’t “see” a dependency inside a function call, leading to bugs where the UI didn’t update.
  • Performance Bottlenecks: Large components with many reactive declarations could trigger unnecessary re-renders because the dependency tracking was coarse-grained.

Runes solve these problems by making reactivity explicit. They use Signals under the hood, a concept popularized by SolidJS and now being adopted by almost every major framework. Signals allow for fine-grained updates, meaning only the exact part of the DOM that depends on a piece of data will update, rather than the entire component block.

1. The Foundation: $state

The $state rune is the bread and butter of Svelte 5. It tells Svelte: “This piece of data will change, and I want the UI to react when it does.”

Think of $state as a smart container. In Svelte 4, you just used let count = 0. In Svelte 5, you use let count = $state(0). This explicit declaration allows Svelte to track the variable across different scopes, including inside classes and external modules.

Basic Usage


<script>
  // Svelte 5 approach
  let count = $state(0);

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

<button on:click={increment}>
  Clicked {count} times
</button>
        

Deep Reactivity with Objects and Arrays

One of the biggest wins in Svelte 5 is how it handles objects. In Svelte 4, you often had to reassign an object (e.g., user = { ...user, name: 'New' }) to trigger an update. With $state, Svelte uses Proxies to make nested properties reactive automatically.


<script>
  let user = $state({
    name: 'John Doe',
    settings: {
      theme: 'dark'
    }
  });

  function toggleTheme() {
    // This JUST WORKS now. No need for reassignment!
    user.settings.theme = user.settings.theme === 'dark' ? 'light' : 'dark';
  }
</script>

<p>Current theme: {user.settings.theme}</p>
<button on:click={toggleTheme}>Toggle</button>
        

Pro Tip: If you have a large array and you only update one element, Svelte 5 is smart enough to update only the specific DOM node tied to that array index, rather than re-rendering the whole list.

2. Computational Efficiency: $derived

Often, you need a value that depends on other pieces of state. For example, if you have a “price” and a “quantity,” you want a “total” that updates automatically. This is where $derived comes in.

In Svelte 4, we used $: total = price * quantity;. In Svelte 5, we use let total = $derived(price * quantity);. The advantage here is that derived values are cached. They only recompute when their specific dependencies change, saving CPU cycles.


<script>
  let price = $state(10);
  let quantity = $state(2);
  
  // This value is computed only when price or quantity changes
  let total = $derived(price * quantity);
  
  // You can even nest derived values
  let formattedTotal = $derived(`$${total.toFixed(2)}`);
</script>

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

What about $derived.by?

Sometimes your logic is too complex for a single line. For those cases, Svelte 5 provides $derived.by, which accepts a code block or a function.


let complexValue = $derived.by(() => {
  if (price > 100) {
    return (price * quantity) * 0.9; // 10% discount
  }
  return price * quantity;
});
        

3. Handling Side Effects: $effect

Programming isn’t just about pure data; sometimes you need to interact with the outside world. This might mean logging data to the console, saving to localStorage, or manipulating the DOM manually. These are called “side effects.”

The $effect rune runs after the component has updated. It tracks its dependencies automatically, just like $derived, but instead of returning a value, it executes an action.


<script>
  let searchTerm = $state('');

  // Automatically sync to localStorage whenever searchTerm changes
  $effect(() => {
    localStorage.setItem('mySearch', searchTerm);
    console.log('Search term saved!');

    // Optional: Cleanup function
    return () => {
      console.log('Cleaning up before the next run...');
    };
  });
</script>

<input bind:value={searchTerm} placeholder="Search..." />
        

Warning: Avoid using $effect to update other state variables if you can use $derived instead. Updating state inside an effect can lead to infinite loops if not handled carefully.

4. Component Communication: $props

Passing data from parent to child is a core part of any framework. Svelte 5 simplifies this with the $props rune. No more export let name; syntax!


<!-- Child.svelte -->
<script>
  // Destructure props with default values
  let { title, description = "No description provided" } = $props();
</script>

<div class="card">
  <h1>{title}</h1>
  <p>{description}</p>
</div>
        

This approach is much more consistent with standard JavaScript destructuring and makes it easier to use TypeScript for prop validation.

Step-by-Step: Building a Reactive Inventory Manager

Let’s put everything we’ve learned into practice. We will build a simple inventory manager that tracks products, calculates total value, and persists data.

Step 1: Set up the State

First, we define our initial list of products using $state.


<script>
  let items = $state([
    { id: 1, name: 'Laptop', price: 999, qty: 1 },
    { id: 2, name: 'Mouse', price: 25, qty: 5 }
  ]);
</script>
        

Step 2: Add Derived Calculations

We need to know the total value of our inventory at all times.


  let totalValue = $derived(
    items.reduce((sum, item) => sum + (item.price * item.qty), 0)
  );
        

Step 3: Create Interaction Methods

We’ll add functions to update quantities. Notice how we can modify the array directly!


  function adjustQty(id, amount) {
    const item = items.find(i => i.id === id);
    if (item) {
      item.qty += amount;
      if (item.qty < 0) item.qty = 0;
    }
  }
        

Step 4: Final Markup


<h2>Inventory Manager</h2>
<ul>
  {#each items as item}
    <li>
      {item.name} - ${item.price} 
      <button on:click={() => adjustQty(item.id, -1)}>-</button>
      {item.qty}
      <button on:click={() => adjustQty(item.id, 1)}>+</button>
    </li>
  {/each}
</ul>

<h3>Total Inventory Value: ${totalValue}</h3>
        

Common Mistakes and How to Fix Them

1. Using $state inside a regular function scope

The Mistake: Trying to declare let x = $state(0) inside a function that runs on every render.

The Fix: Runes should be declared at the top level of your component or inside a class constructor. They are meant to be long-lived references.

2. Overusing $effect

The Mistake: Using an effect to change state that could have been a $derived value.

The Fix: If you are calculating B based on A, always use $derived. Only use $effect for non-Svelte things like API calls or DOM manipulation.

3. Mutating props directly

The Mistake: Trying to change a value received via $props().

The Fix: In Svelte, data flow should be “props down, events up.” If a child needs to change a parent’s state, the parent should pass down a function as a prop for the child to call.

Svelte 4 vs. Svelte 5: A Quick Reference

Feature Svelte 4 (Old) Svelte 5 (New)
Reactive State let count = 0; let count = $state(0);
Derived State $: double = count * 2; let double = $derived(count * 2);
Side Effects $: { console.log(count); } $effect(() => { console.log(count); });
Props export let name; let { name } = $props();

Summary and Key Takeaways

Svelte 5 Runes represent a major leap forward in frontend developer experience. Here are the core points to remember:

  • Reactivity is explicit: Using $state makes it clear what data is reactive and what isn’t.
  • Fine-grained updates: Thanks to the signal-based engine, Svelte 5 is faster and more efficient than its predecessors.
  • Logic is portable: Because Runes aren’t limited to .svelte files, you can share reactive logic across your entire application using simple JavaScript classes or functions.
  • Consistency: The new syntax aligns Svelte more closely with modern JavaScript standards, making it easier for developers coming from other frameworks to get up to speed.

Frequently Asked Questions (FAQ)

Is Svelte 4 code still supported in Svelte 5?

Yes! Svelte 5 is designed with a high degree of backwards compatibility. Most Svelte 4 components will continue to work, though you won’t get the performance benefits of Runes until you migrate them.

Do I still need Svelte Stores?

In many cases, no. $state can be used inside global JavaScript files, which covers 90% of what developers used Stores for. However, for specific use cases like custom streams or complex observable patterns, Stores are still available.

Does Svelte 5 work with TypeScript?

Absolutely. In fact, Svelte 5 was designed with TypeScript as a first-class citizen. $props() and $state() are fully type-safe, making your development process much smoother.

When should I use $derived.by instead of $derived?

Use $derived for simple, single-expression logic. Use $derived.by when your logic requires multiple lines, if/else statements, or temporary variables to calculate the final result.

Ready to start your Svelte 5 journey? The best way to learn is by doing. Open up a new Svelte 5 project today and try replacing your old $: labels with $derived and $effect!