Svelte 5 Runes: The Definitive Guide to Modern Reactivity

For years, Svelte has been the “darling” of web developers who value simplicity. By using a compiler to turn your declarative code into highly optimized JavaScript, Svelte eliminated the need for a Virtual DOM. However, as applications grew in complexity, the traditional reactivity model of Svelte 3 and 4—relying on the let keyword and the $: label—started to show its age. Enter Svelte 5 Runes.

Runes represent a paradigm shift. They move Svelte away from “compiler-magic” reactivity toward a “signal-based” reactivity model. This change makes your code more predictable, more scalable, and significantly easier to debug. If you have ever struggled with $: blocks not firing when you expected, or found it difficult to share reactive state across multiple files, this guide is for you.

In this deep dive, we will explore everything from basic state management to advanced performance patterns using Svelte 5 Runes. By the end of this article, you will have the knowledge to build enterprise-grade applications with the most modern version of Svelte.

The Problem: Why Did Svelte Need Runes?

In Svelte 4, reactivity was tied to the component’s top-level scope. If you declared let count = 0, the compiler knew that changing count should trigger a re-render. While elegant, this approach had three major limitations:

  • Locality: Reactivity was confined to .svelte files. If you wanted to move reactive logic into a separate .js or .ts file, you had to use Svelte Stores, which have a different syntax and API.
  • Granularity: Svelte 4 reactivity was often “all or nothing.” Updating an object usually meant the compiler assumed the entire object changed, leading to unnecessary re-renders.
  • Predictability: The $: reactive declarations could sometimes trigger in confusing sequences, making complex dependency chains hard to follow.

Runes solve these issues by making reactivity explicit and universal. Whether you are inside a component or a standard JavaScript module, the syntax remains the same.

Core Concepts: Understanding the Big Three

Svelte 5 introduces several runes, but three are the foundation of almost every application: $state, $derived, and $effect. Let’s break each one down with practical examples.

1. $state: The New Way to Declare Variables

The $state rune is used to declare reactive data. Unlike the old let syntax, $state makes it clear that a variable is intended to change and trigger UI updates.


<script>
  // Svelte 4 way
  // let count = 0;

  // Svelte 5 way
  let count = $state(0);
  let user = $state({ name: 'John', age: 30 });

  function increment() {
    count += 1;
  }

  function updateName(newName) {
    // Svelte 5 handles deep reactivity automatically!
    user.name = newName;
  }
</script>

<button on:click={increment}>
  Count is {count}
</button>

<input value={user.name} on:input={(e) => updateName(e.target.value)} />
    

Pro Tip: In Svelte 5, $state is deeply reactive. In previous versions, you often had to re-assign an object (e.g., user = user) to trigger an update after changing a nested property. With Runes, simply changing user.name is enough. Svelte uses Proxies under the hood to detect exactly what changed.

2. $derived: Logic That Updates Automatically

Often, you need a value that is calculated based on other reactive variables. In Svelte 4, we used $: doubled = count * 2. In Svelte 5, we use the $derived rune. This is much cleaner and behaves like a standard expression.


<script>
  let items = $state([1, 2, 3, 4, 5]);

  // This updates whenever 'items' changes
  let totalItems = $derived(items.length);

  // You can also use complex logic inside $derived.by
  let sum = $derived.by(() => {
    console.log('Calculating sum...');
    return items.reduce((acc, curr) => acc + curr, 0);
  });
</script>

<p>Number of items: {totalItems}</p>
<p>Sum: {sum}</p>
    

The $derived rune is “lazy.” It only recalculates when its dependencies change and when the value is actually needed. This prevents wasted CPU cycles on values that aren’t being displayed or used.

3. $effect: Handling Side Effects

Side effects are actions that happen outside of the data flow, such as logging to the console, making an API call, or manipulating the DOM manually. The $effect rune replaces the more general-purpose $: for these scenarios.


<script>
  let count = $state(0);

  // This runs whenever 'count' changes
  $effect(() => {
    console.log(`The count is now ${count}`);

    // Optional: Return a cleanup function
    return () => {
      console.log('Cleaning up before the next run or component unmount');
    };
  });
</script>
    

Warning: Use $effect sparingly. If you can calculate a value using $derived, you should always prefer $derived. Effects are for synchronization with external systems, not for transforming state.

Step-by-Step Tutorial: Building a Reactive Inventory Manager

To truly understand Runes, let’s build a small application. We will create an Inventory Manager that tracks products, calculates total value, and persists data to localStorage.

Step 1: Define the State

We need an array of products and a way to add new ones. We will use $state for the array and the form inputs.


<script>
  let products = $state([
    { id: 1, name: 'Laptop', price: 1200, quantity: 5 },
    { id: 2, name: 'Mouse', price: 25, quantity: 10 }
  ]);

  let newName = $state('');
  let newPrice = $state(0);
</script>
    

Step 2: Add Derived Logic

We want to know the total value of our inventory without manually recalculating it every time a price or quantity changes.


<script>
  // ... existing state ...

  let totalValue = $derived(
    products.reduce((acc, p) => acc + (p.price * p.quantity), 0)
  );

  let lowStockAlert = $derived(
    products.filter(p => p.quantity < 3).length
  );
</script>
    

Step 3: Implement Methods to Modify State

In Svelte 5, updating state is as simple as modifying the array. No more products = [...products, newItem] hacks are strictly required, though they still work.


<script>
  // ... previous code ...

  function addProduct() {
    if (newName && newPrice > 0) {
      products.push({
        id: Date.now(),
        name: newName,
        price: newPrice,
        quantity: 1
      });
      // Reset form
      newName = '';
      newPrice = 0;
    }
  }

  function deleteProduct(id) {
    // We can use standard array methods
    const index = products.findIndex(p => p.id === id);
    products.splice(index, 1);
  }
</script>
    

Step 4: Persistence with $effect

We want to save our inventory to the browser’s storage so it survives a page refresh.


<script>
  // ... previous code ...

  // Run this effect once on mount and whenever products change
  $effect(() => {
    localStorage.setItem('inventory_data', JSON.stringify(products));
    console.log('Saved to storage');
  });
</script>
    

Common Mistakes and How to Fix Them

Even seasoned developers might trip up when switching to Runes. Here are the most common pitfalls:

1. Overusing $effect

The Mistake: Using an effect to update one state variable when another changes.


// BAD PRACTICE
let count = $state(0);
let double = $state(0);
$effect(() => {
  double = count * 2;
});
    

The Fix: Use $derived instead. It is cleaner and more efficient.


// GOOD PRACTICE
let count = $state(0);
let double = $derived(count * 2);
    

2. Trying to use Runes in Svelte 4 Components

The Mistake: Runes only work in Svelte 5. If your package.json says "svelte": "^4.0.0", the compiler will throw an error seeing the $ syntax.

The Fix: Upgrade to Svelte 5. If you aren’t ready to upgrade the whole project, you cannot use Runes yet. Svelte 5 is backward compatible, meaning old syntax works in Svelte 5, but new syntax does not work in Svelte 4.

3. Forgetting that $state is Proxied

The Mistake: Expecting $state to work on primitive variables passed into functions by value.

The Fix: Remember that if you pass a number from $state(0) to a function, you are passing the number 0, not the reactive reference. If you need to mutate state inside a function, pass the object containing the state or the updater function.

Advanced Patterns: Reactivity Outside Components

One of the most powerful features of Svelte 5 is that Runes work in .js and .ts files. This effectively replaces Svelte Stores for many use cases.

Imagine a userSession.js file:


// userSession.js
export function createAuth() {
  let user = $state(null);
  let isAuthenticated = $derived(user !== null);

  return {
    get user() { return user; },
    get isAuthenticated() { return isAuthenticated; },
    login(name) {
      user = { name };
    },
    logout() {
      user = null;
    }
  };
}
    

You can then import and use this logic in any Svelte component, and the UI will automatically update when the user logs in or out. This “Universal Reactivity” is a game-changer for state management in large-scale applications.

Svelte 5 Runes vs. React Hooks

If you are coming from a React background, you might compare Runes to Hooks like useState and useMemo. While they share some conceptual goals, Svelte Runes offer several advantages:

  • No Dependency Arrays: In React, you must manually list dependencies for useEffect or useMemo. Svelte 5 tracks dependencies automatically at runtime. No more bugs caused by forgetting a variable in a dependency array!
  • No Rules of Hooks: You can use Runes inside if statements or loops (though usually, you declare them at the top level). You don’t have to worry about the order of execution in the same way React developers do.
  • Native Performance: Svelte Runes don’t require a virtual DOM diffing process. When a $state variable changes, Svelte knows exactly which DOM node to update.

Optimizing Performance with Runes

To ensure your Svelte 5 application runs at lightning speed, follow these optimization tips:

Use $inspect for Debugging

Svelte 5 provides a built-in $inspect rune. It’s like console.log but specifically for reactive state. It will trigger every time the state changes, showing you the old and new values.


<script>
  let settings = $state({ theme: 'dark', notifications: true });
  $inspect(settings); // Automatically logs whenever settings changes
</script>
    

Untrack Certain Values

Sometimes you want an effect to run when variableA changes, but you also happen to read variableB inside that effect without wanting to trigger it when variableB changes. Svelte 5 provides an untrack function for this.


import { untrack } from 'svelte';

$effect(() => {
  console.log(count);
  // This will NOT trigger the effect when 'otherValue' changes
  const data = untrack(() => otherValue);
});
    

Summary and Key Takeaways

Svelte 5 Runes are more than just a syntax update; they are a modernization of the entire Svelte philosophy. Here are the key points to remember:

  • $state: Use it for any data that changes. It provides deep reactivity and works via Proxies.
  • $derived: Use it for data that depends on other reactive variables. It is lazy and highly efficient.
  • $effect: Use it for side effects like DOM manipulation or data synchronization. Avoid using it for state transformations.
  • Universal Reactivity: Runes work in .js and .ts files, allowing you to build reactive logic outside of components without stores.
  • Cleaner Code: No more $: labels or complicated store subscriptions ($storeName). The code looks and behaves more like standard JavaScript.

Frequently Asked Questions

Are Svelte Stores deprecated in Svelte 5?

No, Svelte Stores are not deprecated. They still function perfectly in Svelte 5. However, for most new development, Runes are the recommended way to handle reactive state because they are more flexible and have less boilerplate.

Can I mix Svelte 4 and Svelte 5 syntax?

Yes. Svelte 5 is designed to be backward compatible. You can have a project where some components use the old let and $: syntax while new components use Runes. However, you cannot use Runes inside a component that hasn’t been opted into the Svelte 5 compiler mode (though this is usually handled automatically by the build tools).

Does $state work with Classes?

Yes! This is one of the best parts of Svelte 5. You can use $state inside class fields. This allows for powerful Object-Oriented Programming (OOP) patterns where the properties of your class instances are natively reactive.

How does Svelte 5 track dependencies without an array?

Svelte uses a “signal” architecture. When a $derived or $effect block runs, it sets a global “subscriber” flag. As it accesses $state variables, those variables register the block as a listener. When the $state changes, it knows exactly which listeners to notify.

Building with Svelte 5 Runes is an exciting step forward for web development. By mastering these new tools, you are ensuring your applications are faster, more maintainable, and ready for the future of the web. Happy coding!