Signals in Svelte 5: A Comprehensive Guide to Runes

by Sibbe — 14 minutes

The official release of Svelte 5 is coming soon, and with that Svelte is soon entering the magical era of Runes. Runes are a new concept introduced in Svelte 5, they are primitives that enable you to explicitly declare reactive state, interact with said state, and more. Internally they use signals, a concept that grew in popularity in the past few years, which enable truly fine-grained DOM updates. In this post we'll go over the basic runes available, a couple basic use-cases, what they are useful for, and the problems they are solving. We'll compare Svelte 5 runes with their Svelte 3/4 counterparts, and it'll become clear why they are an essential part of the progression of Svelte.

Why runes?

Runes bring signals to Svelte, a feature that many have been dreaming about, it will make the life of many much easier and their apps more reliable and performant. So why runes? Why not rewrite the internals of Svelte with signals and keep the rest unchanged? Well, one of the main characteristics of runes is their syntax, something that will feel very familiar to people familiar with Vue 3. A lot of thought has been put into this, and the majority of developers will benefit from it.

Previously Svelte used existing JS syntax to implement behavior and to make JS "natively reactive", this was often considered confusing and had a lot of limitations. The solution that Svelte 5 brings is explicit declaration of reactive state and all things around it. Another, very important, reason for runes was enabling the ability to declare reactive state outside of Svelte components. Let's go over each rune, starting with the $props rune to see this in practice.

$props

Props are a very well known concept, they are already present in Svelte 3 and 4, but also in other frameworks like React, Solid and Vue. They are values that are passed to components, and whenever they are updated the component will re-render itself to ensure the application state and DOM are in sync.

Svelte 3 and 4

In Svelte 3 and 4 props were defined using export statements, ie:

<script>
    export let greeting = 'Hello!';
</script>

<h1>{greeting}</h1>

Personally I've always been a big fan of this syntax, and the idea to use existing syntax to declare reactive properties. Many people however considered these odd, and confusing because, what happens when you try to import a prop from the component? What if I try to set it from another file? What if it has been mutated, should the imported value also update? Is it an instance property, or a static property?

Another oddity was declaring rest props, a common scenario is passing arbitrary props to components that should be applied as attributes on the root element of a component. Which, in Svelte 3 and 4, worked a little different than declaring regular props, you wouldn't declare anything, and use $$restProps.<prop> somewhere in the component. Aside from having to know and understand the different syntaxes, and different behaviors behind them, there (for a long time) was no way to properly declare rest props and their types.

Svelte 5

In Svelte 5 defining props works a little different. Defining a prop is simple; you call the $props rune, spread the return value, optionally providing defaults. It looks like this:

<script>
    let { greeting = 'Hello!' } = $props();
</script>

<h1>{greeting}</h1>

We are now clearly defining a property, as we can see from the usage of the $props rune. It works the same as previous versions of Svelte, when the prop is updated by the parent it will update the components that use it.

What about defining rest props? You can probably already guess it, that looks like this:

<script>
    let { greeting = 'Hello!', ...props } = $props();
</script>

<h1>{greeting}</h1>

props now contains an object with the remaining props, it's reactive, works the same as any other prop, and no extra syntax to remember or understand.

$state

The $state rune is used to declare reactive state, often internal to a component. It's a variable that, when mutated, automatically updates the DOM wherever it is used. In previous versions of Svelte all variables declared in a component were reactive.

Svelte 3 and 4

<script>
    let count = 0;
</script>

<button on:click="{() => count++}">Count: {count}</button>

Svelte 5

A small change has happened here, one that some say they hate, but many more say they love. To do the same thing in Svelte 5 you use the $state rune to hint to the compiler that you're declaring reactive state:

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

<button onclick="{() => count++}">Count: {count}</button>

One of the main advantages of this is that it is now possible to declare non-reactive state in components. It's a use-case that often come up in the Github issues of Svelte, where developers wanted to create a variable that did not trigger DOM updates.

$derived

The $derived rune is used to declare derived, or computed, state. It is used to derive a value of other variables, and the derived value is cached in memory. Whenever the dependent values update, the cache is flushed, the derived value is re-calculated, and the DOM is updated accordingly. This makes it easy to declare reactive state that depends on other reactive values.

Svelte 3 and 4

We're now entering a territory where many complexity and oddities arose in previous versions of Svelte. Svelte 3 and 4 used labels (a mostly unused feature in JS land) to declare reactive values, that looked something like the following:

<script>
    let count = 0;
    let doubled = count * 2;
    
    $: doubled = count * 2;
</script>

You can see here that we declare two variables; count and doubled. The initial state of the doubled variable is set, after which it's kept up-to-date by the $: doubled = count * 2 statement. You might already be able to tell that there is an issue here, we have to declare the logic to derive the value twice, on top of this many people find the syntax to verbose. So more typically you would see the following shorthand:

<script>
    let count = 0;
    
    $: doubled = count * 2;
</script>

Still, there are quite some issues with this, as it doesn't work quite the same as the initial example. With this syntax the initial value is never set, and a rough equivalent of this would rather be:

<script>
    let count = 0;
    let doubled;
    
    $: doubled = count * 2;
</script>

Without going too much into the details, this will cause some problems and confusion, because if you added a console.log statement at the end of the script tag, it would actually log undefined. There are ways to work around this, like the initial example, but it remains a great source of confusion. To make things worse, this relies fully on the ability of the compiler to reason about which values are reactive, and which aren't. So if you have a function that references some reactive values, and use that to set the derived values it would not work in many scenarios.

Svelte 5

Svelte 5 solves these problems with the $derived rune, when used to declare a variable it declares a derived state of values used in it, and since internally it relies on signals it doesn't matter how they are referenced, if they're read at any point during the calculation Svelte will know that it needs to listen to changes. In the following example we create the same doubled value using the derived rune:

<script>
    let count = $state(0);
    let doubled = $derived(count * 2);
</script>

This is the only proper way to declare derived state, it declares a variable named doubled which will always be count time 2. The compiler expands the declaration, but it no longer tries to determine which values are or are not reactive, as that's now delegated to signals at runtime.

$effect

The $effect rune is a new concept in Svelte, there many ways to produce similar behaviors, but there is nothing truly like it. If you're coming from react, you should be familiar with it, in many ways it works the same as the useEffect rune. In its foundation, the $effect rune allows you to declare a side-effect of reactive values that is triggered every time one, or more, of its dependencies change. It also allows for you to return a function which will be triggered before the effect itself is triggered which allows you to cleanup the effect. The first time it is triggered is directly after the component is mounted, this means it is not triggered on the server.

Warning: the $effect rune is intended to deal with very specific cases, and most of the time, when you catch yourself wondering if you should use it, the answer is probably: no. (see this section of the preview docs for more details)

Svelte 3 and 4

For people coming from previous versions of Svelte this example should shed some light on what this means. One similar scenario would be the onMount and onDestroy lifecycle hooks:

<script>
    let interval;
    let timeElapsed = 0;
    let delay = 1000;

    $: start(delay);

    function start(delay) {
        if (interval) {
            clearInterval(interval);
        }

        interval = setInterval(() => {
            timeElapsed += delay;
        }, delay);
    }
</script>

<label>
    Delay: <input type="number" bind:value={delay} />
</label>

<div>Time elapsed: {(timeElapsed / 1000).toFixed(2)}</div>

This seems mostly fine until you start realising what's going on, as this relies of a bunch of quirks outlined in the section about the $derived rune. We created a start function which get triggered whenever delay changed, and only when delay changes. For example, if we didn't use a function to wrap this logic it would created an infinite loop.

This also runs during Server-Side rendering, risking odd behaviors and hydration mismatches. So if you wanted to make this work properly you would need to add a bunch of extra code to handle all the edge cases.

Svelte 5

The $effect rune solves exactly this scenario, it only runs in the browser, and it separates the effect and cleanup logic, preventing all the issues mentioned you would run into in Svelte 3 and 4. The ability to inline all this into the effect run is also great for simple cases.

<script>
    let timeElapsed = $state(0);
    let delay = $state(1000);
    
    $effect(() => {
        const interval = setInterval(() => {
            timeElapsed += delay;
        }, delay);
        
        return () => clearInterval(interval);
    });
</script>

<label>
    Delay: <input type="number" bind:value={delay} />
</label>

<div>Time elapsed: {(timeElapsed / 1000).toFixed(2)}</div>

In this example the power of runes, really becomes apparent, let's go through it and see what's going on here. We declared a two reactive variables, timeElapsed, and delay, the timeElapsed needs to be reactive, so we can update the UI whenever it changes, the delay needs to be reactive so we can start a new interval whenever it changes. The interval does not need to be reactive, because it's only used internally to cleanup the interval whenever the effect gets triggered, even better: it can now be scoped to the effect!

The logic to manage the side-effect became a lot simpler, we pass a function to the $effect rune to start an interval, since the function directly references delay in the body it'll be triggered every time the value changes. timeElapsed however is referenced asynchronously, meaning it will not trigger the effect when it changes, preventing infinite loops. Finally, whenever the effect is triggered, as well as when the component is destroyed, the returned cleanup function will be triggered which will clear the existing interval, so that we can prevent memory leaks.

Using runes outside of Svelte components

One thing many people are excited for in Svelte 5 is the ability to declare reactive state outside of component files. Though technically speaking this is already possible using stores, many feel that the ergonomics around them leave much to be desired. In Svelte 5 this will be improved significantly, and it works the same as inside components. All you have to do is create a file suffixed with .svelte.js or .svelte.ts and start using runes inside them.

Defining state in external files

Let's have a look at a simple example using a counter store, we'll create a file called counter.svelte.js and define a factory inside it which creates a new instance of our counter store.

// counter.svelte.js

export function createCounter(initialCount = 0) {
    let count = $state(initialCount);
    
    return {
        get count() {
            return count;
        },
        set count(value) {
            count = value;
        },
        increment() {
            count++;
        }
    }
}

Here we've created a createCounter function which accepts an initialCount parameter to set the initial value of the counter. A variable named count is declared which holds the internal state of the counter. Finally the function returns an object which gives the consumers access to the counter's state, and an increment method. Note that it uses accessors to interact with the state, as we need to provide consumers access to the signals in order to propagate the reactivity, if we used a simple property it would lose reactivity the moment it is returned. This has little to do with Svelte, and everything to do with the nature of JavaScript, as primitives values can not be watched.

To consume the counter, all we need to do is import the factory, create an instance of the counter, and reference it in the template somehow:

<script>
    import { createCounter } from './counter.svelte.js';
    
    const counter = createCounter();
</script>

<button onclick="{() => counter.increment()}">
    Count: {counter.count}
</button>

Defining state as class fields

Defining accessors for all state values can become increasingly tedious as the store becomes more complex, and more values are exposed. If you don't want to go through the hassle of maintaining accessors, internal state, or you just don't like factory functions, you can use classes instead. Public class properties will automatically be converted to private properties and accessors referencing the private properties. For example, the following class is the equivalent of the factory defined above:

// counter.svelte.js
export class Counter {
    count = $state();
    
    constructor(initialCount = 0) {
        this.count = initialCount;
    }
    
    increment() {
        this.count++;
    }
}

A small adjustment to the consuming component adjusts it to use the class, all we have to do is replace the call of the factory with an instantiation using the Counter class:

<script>
    import { Counter } from './counter.svelte.js';
    
    const counter = new Counter();
</script>

<button onclick="{() => counter.increment()}">
    Count: {counter.count}
</button>

Conclusion

Runes open up a whole new world for Svelte, and even though I do have some concerns (which I haven't mentioned in this post), and many people started complaining when they first saw it, opinions like "It's just React without JSX", or "It's just a worse version of Vue 3" quickly dissipate when people start using Svelte 5. As for me personally: ever since I started using Svelte 5 with the initial release, it's become hard for me to go back to previous version of Svelte. The improved ergonomics of inter-component communications, and the simplification of many of the edge cases I've often been bothered by, have improved the quality-of-life when developing in Svelte significantly, something of which I didn't think it would be possible.

If you would like to see more examples of Svelte 5 runes, and how they compare to previous versions, or perhaps other frameworks, check out component-party.dev.

Some other resources worth checking out:

meerdivotion

Cases

Blogs

Event