The release candidate of Svelte 5 has been out for some time. One of its most discussed features is the introduction of “runes” as another approach to reactivity in Svelte, which is already famous for its unique way of declaring reactive states.
In this article, we will see how to use the new runes system to declare reactive states, how this system is different from the existing one in Svelte 4, and some examples to compare them. To follow along, you should already have some familiarity with Svelte and how it’s set up and used.
Svelte 5 is one of the most anticipated updates for the Svelte framework. It brings several changes to Svelte 4, introducing some new features while deprecating some old ones. Let’s go through some of the notable changes.
Svelte 5 has added the notion of snippets, reusable markup code inside a component itself. You can think of a snippet as a component within a component.
Similar to how we can use components to extract repeated code into one place, snippets allow us to extract repeated markup within a component. These are local to the components, and must be defined in the same file as the component.
Snippets can help reduce the duplication of markup within a component, which would be useful for cases where something is repeated within a component several times, but not outside the component. However, keep in mind that this doesn’t justify extracting a snippet as a dedicated component.
Svelte’s event handling system has been updated drastically in two major ways:
on:
directives such as on:hover
or on:click
for handling events on components, the event handlers are now supposed to be passed in as props, and the names are changed to onhover
and onclick
correspondinglycreateEventDispatcher
, we now simply take the event handler as a prop and use it as a callback when that event occursThe original directives using on:
still work, but are now deprecated and planned for removal in some next major release of Svelte.
Svelte 5 exposes several new functions for developers to use directly. For example, mount
and unmount
functions are provided for manually mounting and unmounting a component on a specific DOM element.
Also, svelte/reactivity
now exports map
, set
, date
, and url
classes, which can be used in place of native JavaScript ones and are reactive by default.
The introduction of runes is probably the most discussed change in Svelte 5. The new runes system has had both positive and negative reactions from the community. We will be exploring runes in detail in the rest of this article.
Before seeing runes and how they can be used for reactivity in Svelte 5, let’s briefly take a look at how reactive state is managed in Svelte 4.
Reactivity is one of the main advantages of any frontend framework. Without it, we’d have to manually track where a particular value is used and then manually update the DOM when the value changes. With it, we can mark certain values as “reactive” and let the framework take care of the tracking and DOM updates.
Different frameworks handle reactivity in different ways. For example, React has Hooks in its functional components, while Vue uses refs and reactive objects.
One of the major ways Svelte differs is that it lets you define reactive states like normal JavaScript variables. It then uses a compiler to detect dependencies and inject updated code into the DOM when the value changes. This makes state declarations look like normal JavaScript declarations and adds certain level of familiarity when using them.
For example, if we assign a value to a variable like so:
<script> let value = 1; </script>
And then use this in our HTML markup:
Value is <em>{value}</em>
Svelte will track our value
variable and automatically update the DOM whenever we change this value somewhere else, such as:
<button on:click={ ()=>{ value += 1; } } > Click Me! </button>
However, this approach has some limitations. As the changes are tracked by assignment, this does not work as smoothly with arrays and objects. For example:
let arr = [1,2,3]; ... <ul> {#each arr as v} <li>{v}</li> {/each} </ul> <button on:click={ ()=>{ arr.push(0); } } > Click Me! </button>
Here, we would expect the list to get updated with each button push, as we are updating the array by pushing 0
on each click. However, as there is no assignment, the compiler does not consider this as an update and the list does not change.
To make this work, we need to add a dummy assignment arr = arr
after the push like this:
<button on:click={ ()=>{ arr.push(0); arr=arr; } } > Click Me! </button>
Similarly, Svelte does not track transitive reactivity. For example:
<script> let x = 0; let y = 5 * x; </script> ... <h2>{x} {y}</h2> <button on:click={ ()=>{ x = x+1; } } > Click Me 2! </button>
Here, y
is defined using x
, and updating x
should ideally also update y
. However, Svelte doesn’t do this by default. For this to work, we must use a reactive declaration like so:
$: y = 5 * x;
While Svelte tracks most of the reactivity on its own, it needs some help in such cases to make sure all dependents are updated properly.
To fix these and several other issues, Svelte 5 has introduced the concept of runes. They function like symbols and provide instructions to the compiler about reactivity and dependencies. Runes are essentially JavaScript functions to which Svelte’s compiler pays special attention. They can also be used in a .svelte.js
file.
Note that runes are opt-in in Svelte 5. In other words, the previous approach to reactivity in Svelte will work as before, and we can selectively use runes if we want.
The most basic rune is the $state
rune. This is similar to the let
declaration in Svelte 4, and marks a variable as a reactive, changeable state. With this, the first example would get updated to:
<script> let value = $state(1); </script> Value is <em>{value}</em>. <button onclick={ ()=>{ value += 1; } } > Click Me! </button>
Instead of the let
declaration itself marking reactivity, now the $state
rune returns an object which emits signals when it is updated. Updating the value with value +=1
is unchanged, but we’re using the new event system by onclick
instead of on:click
. This behaves exactly same as the Svelte 4 example.
One of the advantages of the rune system is that this also works seamlessly with arrays and objects, unlike the let
declarations:
<script> let arr = $state([1,2,3]); </script> ... <ul> {#each arr as v} <li>{v}</li> {/each} </ul> ... <button onclick={ ()=>{ arr.push(0); } } > Click Me! </button>
This will correctly update the list without needing the extra assignment statement.
Another rune is the $derived
rune, which indicates that the assigned variable is derived from the input variables, and thus should be updated when any of them changes. If we update the x-y example we saw before in Svelte 4 using this rune, we’d have the following:
<script> let x = $state(0); let y = $derived(5 * x) </script> <h2>{x} {y}</h2> <button onclick={ ()=>{ x = x+1; } } > Click Me 2! </button>
Here, instead of the $:…
declaration, we define y
using the $derived
rune. This indicates that the value of y
is derived using x
and should get updated when x
changes. Apart from this, there is no other change, and updating x
also updates y
correctly.
Svelte 4 also had the concept of reactive statements, where we use $:
to prepend a statement such as console.log(…)
or if(…){…}
. This indicates that the statement should be re-run each time there is any update to any of the variables used in that statement.
We would usually use reactive statements in cases such as debugging a variable update, making an API call to update search results, and some others. Now, Svelte 5 has $effect
rune, which does the same job:
$effect(()=>{ console.log(x,y); });
The compiler will detect the dependencies and run the callback each time any of them is updated.
For the debugging use case, we have another interesting rune called $inspect
. This rune logs the values passed to it whenever any of those values get updated. We can also pass a custom call back to the rune, which will be invoked instead of logging the values.
For example, instead of using the $effect
rune above, the same effect can be achieved like so:
$inspect(x,y);
You can also accomplish this using a custom callback:
$inspect(x,y).with((updateType,newX,newY)=>{ if(newY !== newX*5){ console.error("Something is wrong!!!"); } });
The first argument to the callback is a string with a value of either init
or update
. This specifies whether the callback is run to initialize the variable or for subsequent changes to the value. The rest of params are variables passed to the $inspect
in same order, with new values.
The new runes system in Svelte has been discussed a lot in issues, Reddit threads, and other places. As we’ve mentioned, there have been both positive and negative reactions to this change. But since the release candidate is out, runes are here to stay — at least in Svelte 5.
Let’s compare the old way of reactivity and runes. You’ll see some of my personal opinions on the new system, so keep that in mind.
The previous syntax for reactive declarations used let .. = ..
statements. In comparison, runes are slightly more verbose.
However, when comparing runes to React Hooks, I don’t think they are too verbose. Hooks have a value and a separate update function, while runes still use normal, assignment based updates. Runes feel somewhat similar to Vue’s ref system, but with some significant differences.
While they’re not like every other JavaScript frontend framework, runes — as I’ve said — do remind me of React’s Hooks or Vue’s refs. Svelte’s simple JavaScript-like declarations of reactive values was one of its unique points, so this change feels like it makes Svelte slightly more similar to other frameworks.
While some might strongly dislike this similarity, it also has some positives. For example, this similarity might make Svelte easier to use for devs who are more experienced with other frameworks. As a result, introducing runes may lead to Svelte getting more widely adopted.
As seen above, the current Svelte compiler has certain issues with arrays and objects. While runes might not solve all of the issues — for example, objects are still not deeply reactive, only on shallow level — they solve several of the annoying pain points that are experienced more commonly when dealing with arrays and objects.
Also, runes allow us to use reactivity in .svelte.js
files. So, instead of having to understand and use stores, we can use the reactivity primitives used everywhere else in the app.
Additionally, runes also help with some performance issues. As explained in the Svelte 5 docs, updating an item in an array cased the whole array to be invalidated, not just the changed item. Now with runes, updates can be more fine grained, thus saving some processing and rendering time.
Currently, any let
declaration can be a reactive state that changes and updates the app. Similarly, any $:…
can be a dependency, a debug statement, or a side-effect.
With runes, there are dedicated ways to express these intentions. Thus, you can be more certain of the intention behind the declarations just by reading the code.
Runes are a new addition to the upcoming Svelte 5 release that are available to use in the release candidate. While runes solve several issues with Svelte reactivity, there are certainly still some pain points — as with any significant changes to a framework.
What do you think of this change? Let me know in the comments!
Thanks for reading. You can find more info in the Svelte 5 docs and try out runes in the Svelte 5 playground.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowHandle frontend data discrepancies with eventual consistency using WebSockets, Docker Compose, and practical code examples.
Efficient initializing is crucial to smooth-running websites. One way to optimize that process is through lazy initialization in Rust 1.80.
Design React Native UIs that look great on any device by using adaptive layouts, responsive scaling, and platform-specific tools.
Angular’s two-way data binding has evolved with signals, offering improved performance, simpler syntax, and better type inference.