The Guide to Svelte Runes

| Published: January 25, 2024

| Updated: January 30, 2024

Runes have been the flagship feature of the upcoming Svelte 5 release, set to drop early 2024. On the announcement day of Svelte 5, the Svelte team released their work-in-progress implementation of Runes, with some accounting of why they’re necessary.

The Magical Svelte Runes

So far, the implementation has had mixed reviews. Some people in the Svelte ecosystem think the more verbose syntax of runes defeats the purpose of using Svelte in the first place. Others appreciate having a more reliable source of truth when it comes to using reactivity beyond .svelte files. But we’ll get to that later…

No matter how you slice it, runes are a significant change to how we will develop apps in Svelte. So with that said, it’s a good idea to start getting familiar with the new concepts and syntax as we move onto the next chapter of the Svelte story.

In this article, we will:

  • walk through all the additional syntax you’ll need to learn,
  • outline instances where you will use runes,
  • mention the now-obsolete keywords and patterns from Svelte 4, and
  • talk a bit about what’s happening under the hood

So strap in, we’re going to rune-town. 🤩

Skip to FAQ’s

The basic syntax

Before runes, you would typically declare a reactive variable like this:

let points = 0;

Now with runes, this is how your write a reactive variable:

let points = $state(0);
It looks like React

Ok king, I hear ya. That ”state” keyword has really got some developer’s knickers in a knot.

And yes, it’s slightly more verbose than the elegant way we’ve gotten used to, however there’s a method to the madness.

If you want a quick answer for “why” runes have been introduced to Svelte, it’s simply more consistent and interopable throughout the different files and sections of your app.

Prior to runes, the behaviors inside .svelte components have been odd to say the least! So now we have runes that can be called in both .ts files and .svelte files, so you can shared behaviors (like reactivity) outside of .svelte files. This is particularly useful when creating writables stores in .ts files.

You can skip to read more reasons why runes are an important addition to Svelte, but let’s jump in an start using them first!

How can I start using Runes?

Runes are a feature coming with Svelte 5, which is still currently in it’s beta stage. Depending on how serious you are about using Runes right now, you have 2 options available to you:

  1. The Runes Playground, or the
  2. Creating a new project with Svelte 5 features (including runes)

If you want to learn about and play with all of the Svelte 5 features, you can check out the guide to Svelte 5. It’s more succinct on the topic of runes, but it encompasses all the other new featurs and breaking changes with v5.

The runes playground

The Svelte team have made an online playground available so the people can start playing with runes before incorporating them into their project.

via GIPHY

The playground is the REPL you’ve probably seen around, but this one has runes functionality baked into the .svelte components.

You can visit the runes playground on the Svelte 5 preview site. Try porting some components from your current projects and go through the conversion process as we visit all different runes in this article.

Starting a new project with Svelte 5

At the moment, when you start a new project following the standard Svelte installation process, they give you the option to opt into Svelte 5 features, including runes.

Make sure to select the Svelte 5 preview beta

This will set the syntax to use runes (and other Svelte 5) features by default. As Svelte 5 is still in beta and the API’s are likely to change, it might not be a good idea to use this for mission-critical apps in production.

But if you’re happy to, then you can just start using runes in your new project. You can scope the rune-based compile either way by:

  1. Enforcing them on a component-by-component bases by opting in with a <svelte:options> tag, or
  2. Force the compiler to use runes project-wide in your svelte.config.js file.

I you want to use either the old compile or the runes compiler in a single component, you can add this tag:

<!-- This can also be false if you want to use the Svelte 4 compiler/syntax -->
<svelte:options runes="{true}" /> //

Or you can force runes (or the old syntax) project wide by adding runes: true to your compilerOptions in your svelte.config.js file:

export default {
	compilerOptions: {
		runes: true // Which could also be false if you want to force the Svelte 4 compiler/syntax
	}
};

So now that you’re set up, lets look at all of the runes we have available to us! 👯‍♂️

via GIPHY

All of the runes declaration types

That $state example we just used above is just the tip of the iceberg. There’s quite a bit to discuss when talking about runes, so let’s walk though all of the new declaration types, and when to use them.

The rune declarations are:

You’ll likely use most of them regularly, so it’s worth taking the time to understand how each works. They’re roughly ordered by level of importance, from “must-knows” to “nice-to-knows”.

via GIPHY

The $state rune

Use the $state rune declaration to make a reactive variable. You can do that in a .svelte <script> tag, or in any .ts file like this:

let points = $state(1);

We’ve talked about this already because it’s the most simple and useful rune primitive.

Unlike the old way of declaring reactive variables, which can only be declared inside <script> tags in .svelte components, a $state rune can be declared in your .svelte components, and also your .ts files. This means you have no unexpected behaviors when sharing reactive variables between these files.

Another huge plus to this is that $state objects and arrays are reactive and can be mutated. For example:

<script>
	let purchaseValueInput = $state(0);
	let allPurchases = $state([22, 30, 18, 26]);
</script>

<input bind:value="{purchaseValueInput}" type="number" />

<button
	onclick={() => {
		allPurchases.push(purchaseValueInput);
	}}
>
	Add Purchase</button
>

<button
	onclick={() => {
		allPurchases.pop();
	}}
>
	Refund last purchase</button
>

<p>Total: ${allPurchases.join(' + $') || 0} = ${allPurchases.reduce((a, b) => a + b, 0)}</p>

And the result is a completely reactive array which we would treat as any normal Javascript array.

You can mutate the array and it works

The $state.frozen rune

The $state.frozen rune is similar to the regular $state rune, however the value cannot be mutated, however it can be reassigned.

via GIPHY

The state.frozen rune is similar to Reacts useState in that you can’t mutate the state, but when you use setState(), you just reassign a new value to it.

From our last example, this won’t work:

<script>
	// ... other variables
	let allPurchases = $state.frozen([22, 30, 18, 26]);
</script>

<!-- ❌ This will not work -->
<button
	onclick={() => {
		allPurchases.push(purchaseValueInput);
	}}
>
	Add Purchase</button
>

<button
	onclick={() => {
		allPurchases.pop();
	}}
>
	Refund last purchase</button
>

When we click either of the buttons, we’re attempting to mutate a $state.frozen rune directly, so we get this error:

Error: Cannot add property 0, object is not extensible

And nothing happens to the allPurchases rune.

Instead, if you wanted the same effect to work, you would assign it like this:

// ✅ This will work
<script>
	// ... other variables
	let allPurchases = $state.frozen([22, 30, 18, 26]);
</script>

<button onclick={() => allPurchases = [...allPurchases, purchaseValueInput]}>Add Purchase</button>
<button onclick={() => allPurchases = allPurchases.slice(0, allPurchases.length - 1)}>Refund last purchase</button>

The benefit of $state.frozen is that it improves performance, especially when dealing with large arrays or objects. This is because it doesn’t have to build out the small cost of having to make it reactive.

via GIPHY

There’s also a case to be made that using $state.frozen is safer if you wanted to make sure the object/array you were declaring doesn’t mutate at any point. Kind of like using a const instead of a let in vanilla Javascript.

The $derived rune

The $derived rune is a reactively updating variable which is dependent on the result of some function.

The $derived rune will always be reactive to the changes of its dependencies. For example, I want the $derived variable to be 10x the base value.

<script>
	let base = $state(1);
	let tenX = $derived(base * 10);
</script>

<input bind:value="{base}" />
<p>10x = {tenX}</p>

In this example, you have an input, and below it is the 10x value of it, which updates with the base value in the input:

It should say 420.69 but the number system has to ruin it for me.

(╯‵□′)╯︵┻━┻ I promise, it works

The $derived value will also always be current, so you don’t need to worry about value mismatches when using the derived value in a function right after the dependency changes - your changes will be reflected immediately, and thus before the next executable runs.

It’s also worth noting that you can’t use state changes inside your derived functions - it’ll cause an infinite loop and Svelte will error at compilation.

via GIPHY

The syntax change

The $derived rune has similar functionality to the old method of declaring a variable with $:. For example:

// ❌ Old derived method
let base = 5;
$: tenX = base * 10;

should now be:

// ✅ New derived method with runes
let base = $state(5)
let tenX: $derived(base * 10)

But the differences here is that $derived runes (new way), track dependencies at runtime, rather than at compile time. This means that you can now outsource your statements to a function, and the derived value will track without explicitly declaring the dependencies inside the derived statement.

You don’t need to explicitly declare dependencies in your $derived statement anymore

One of the caveats when dealing with the old $: syntax was that in order to maintain reactivity, the dependent variables had to be explicitly present in the derived statement (inside the brackets).

For example, in our 10x example above, if we were to go back to the old syntax and outsource our ”statement” to a separate function, like this:

<script>
	const getTenx = () => base * 10;
	let base = 1;
	$: tenX = getTenx();
</script>

<input bind:value="{base}" />
<p>10x = {tenX}</p>

Then the derived tenX would not be reactive to the base variable, because all it’s pointing to is a function. Whilst the return value of the function might change, the function itself won’t.

As mentioned earlier, this is because dependencies were tracked at compile time, and now they’re tracked at runtime. So now we can have our dependencies tracked from within a function, like this:

<script>
	const getTenX = () => base * 10;
	let base = $state(1);
	let tenX = $derived(getTenX());
</script>

<input bind:value="{base}" />
<p>10x = {tenX}</p>

And the $derived value reacts as you expect it would!

via GIPHY

This is particularly helpful when you have more complex functions with if statements. As with the old way, you’d have to run a function every time a value changes, and specifically update a variable within that the function.

Reactive derived variables now work better with Typescript

In Svelte v4, it was all well and good to create a reactive variable with the $: syntax, however if you also wanted to assign a type to it, your only option was to assert it with an as keyword.

For example:

<!-- ❌ Incorrect syntax -->
$: division: number | boolean = height > 150 ? height : false;
<!-- ✅ Correct syntax but not as typesafe -->
$: division = (height > 150 ? height : false) as number | boolean;

For whatever reason you couldn’t infer a type for the reactive variable, asserting it was the only option.

Now with runes, it’s native:

let division: number | boolean = $derived(height > 150 ? height : false);

Now let’s look at the new runes way to accept props in a .svelte component.

The $props rune

The $props rune is the new way to accept props into a .svelte component.

Instead of using export let, you can now destructure them using a $props call.

For example, instead of declaring component props like this:

// ❌ The Old Way

export let propA;
export let propB;
export let propC;

You now declare them like this:

// ✅ The New Way

let { propA, propB, propC } = $props();

Or if your using typescript, you would declare your props like this:

// ✅ The New Way with typescript

type Props = {
	propA: string;
	propB: number;
	propC?: boolean;
};

let { propA, propB, propC = true } = $props<Props>(); // For propC, the default value is true when no value is passed

The change isn’t extraordinary, however the support for typescript is slightly improved. I particularly love the typescript users can use the optional operator (?) again for prop types, instead of having to write | undefined.

The new $props rune can also help with code readability as it keeps all props together in 1 line of code, which is something many other Javascript frameworks do. The difference is that instead of seeing your props explicitly stated on their own line with the associated type, you’ll have to refer to where the type is declared.

You can continue using:

  • export const, and
  • export function in the same way you always have, however with the $props rune, the export let keyword gets the chop.

via GIPHY

As one of the most loved parts of Svelte, sunsetting the `export let` method of exposing props is a bold move. The upside is that for developers coming from frameworks like React, the transition should be seamless.

For advanced users and library builders, the new $props rune also replaces the $$props and $$restProps keywords. This would be replaced by rest syntax, which you can used like this:

let { propA, propB, propC, ...rest } = $props();

where:

  • $props can will replace $$props, and
  • The rest syntax ...rest can cover the $$restProps

It doesn’t matter what you write in place of rest, by the way - as long as you prefix it with ..., the compiler knows you’re declaring the rest of the props.

The $inspect rune

The $inspect rune is available in development environments, which triggers a console.log of its arguments when any of them change.

So, if you wanted to log changes to the state variables, points and isGameOver, you could declare this:

<script>
	let points = $state(0);
	let isGameOver = $state(false);

	$inspect(points, isGameOver); // will console.log when `points` or `isGameOver` change
</script>

So, this will do 2 things:

  • When the component is initialized, $inspect will log an "init" string, followed by the initialized value of the arguments, points, and isGameOver
  • Then, if either of the points or isGameOver values change, the $inspect rune will log an "update" string, followed by the updated values of both arguments at the time it is called.

So, on page load, we get this log:

"init"
0
false

and then if I click a button to increment the points value, we get this log:

"update"
1
false

And then if I click the game to be over, it logs this:

"update"
1
true

Pretty straightforward - it’s basically a reactive statement with a console.log attached to it. If you wanted to emulate a similar pattern in Svelte 4, you’d do something like for the same effect:

let points = 0;
let isGameOver = false;

$: points | isGameOver, console.log(points, isGameOver);

However, you don’t get the in-built ability to distinguish between init and update without building out a bunch of other code to handle this.

Let’s double-click into the init and update distinction a little more, as it’s part of the with property which you can extend the $inspect rune with.

via GIPHY

The with property in the $inspect rune

As we just discussed, the $inspect default behavior is to log its arguments when they are initialized, or when they change. You can modify this default behavior however, to anything you want, using the with property.

let points = $state(0);

$inspect(points).with((type, count) => {
	console.log('Number of times triggered: ', count);
	if (type === 'init') {
		console.log('We are only just getting started');
	}
});

When you append with() to your normal $inspect rune, you can extend out the functionality of it, and the callback arguments provided to you are:

  • type, and
  • count

Where type can be either "init" or "update" - which we discussed before as it logs with the default behavior.

And the count callback argument is the amount of times it has been triggered in its lifecycle.

Use the $inspect run for debugging

As the $inspect rune is only available in development environments, it’s best explained by showing how it can be used with the debugger:

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

	$inspect(points).with((type, count) => {
		if (type === 'update') {
			debugger;
		}
	});
</script>

<button onclick={() => points++}>Add Point</button>

Which would open up the debugger every time a point is added. Make sure you have your browser DevTools panel open for this to work.

The DevTools Debugger

With the debugger panel open, you can go through the source code and step over the processes to better understand what’s making the unexpected changes you see in your application. If you aren’t familiar with the debugger panel, your debugging skills will level up by learning how to use it.

via GIPHY

Another shortcut available to diagnose where changes are coming from with the $inspect rune is to use:

$inspect(variable).with(console.trace);

Which gives an output that looks like this:

The change trace log

Which logs the type and the argument value on the top line, followed by the stack trace leading changes that occur leading up to the log from the bottom up. This way you can see if there’s any rogue function calls happening that you didn’t expect.

This changes so far have mostly been received well, but now we’re moving into territory which has rattled some Svelte developers.

Warning: React-like syntax incoming 😏

The $effect rune

The $effect declaration runs a side effect when some dependent variable changes.

This is fairly loaded and can be used for many different purposes, however if you’ve used React before, it’s very similar to the useEffect(). You know, that hook that everyone loves. 😅

via GIPHY

In it’s basic form, it looks like this:

$effect(() => {
	console.log({ receivables, payables });

	return () => {
		console.log('cleanup');
	};
});

What’s happening here is:

  • when the component is mounted, anything above the return statement gets run, so in this case, the value of receivables and payables is logged,
  • every time the value of either the receivables or payables change, the part above the return statement runs, so in this case it will log the updated values of these values
  • when the component is removed from the DOM, the part inside the return statement is run, so in this case it logs “cleanup” when the component is destroyed, and also
  • every time this effect has to re-run, the part inside the return statement is run before it runs the top section.

So if we had this component:

<script>
	let receivables = $state(0);
	let payables = $state(10);

	$effect(() => {
		console.log({ receivables, payables });

		return () => {
			console.log('cleanup');
		};
	});
</script>

<label>     Receivables     <input bind:value="{receivables}" type="number" /> </label>

<label>     Payables     <input bind:value="{payables}" type="number" /> </label>

The logs on page load are:

{ receivables: 0, payables: 10 }

This is because the ‘section above return’ runs when the component mounts.

And with every number you press in either of the inputs, this is logged to the console:

"cleanup"
{ receivables: 0, payables: 100 }

Remember, the return statement is run immediately before every $effect re-run. And because changing either of the dependencies inside the $effect triggers a re-run, the 'cleanup' string is logged.

via GIPHY

Note: the return statement in any $effect rune is optional.

You can even just place a variable in the $effect function without any direction, just to add it to the dependency list.

For example, if we want to log “Things are changing” every time one of our variables change, but we don’t necessarily want to do anything with it in our $effect rune, we can place it in there like this:

let numbers: $state([])
let letters: $state('')

$effect(() => {
	numbers;
	letters;
	console.log("Things are changing");
})

Notice we’ve just placed them in there as dependencies, but no instruction is running using the numbers or letters variables.

What the $effect rune is for

You can use the $effect rune to:

  • Do something onMount(),
  • Do something afterUpdate(),
  • Do something onDestroy(), and
  • Perform some action (e.g. fetch call or log) when a variable (an $effect dependency) changes.

Previously with Svelte, you would perform such a task like this:

$: dependency, effectFunction();

Which meant that every time dependency changed, the effectFunction would run. And you could stack them, like this:

$: dependency1, effectFunction();
$: dependency2, effectFunction();

So, love it or hate it, $effect can make your codebase more concise.

via GIPHY

The argument of introducing the footguns that React has with useEffect is not entirely valid. This is because useEffect is required for making derived values, and state values aren’t actually reactive like they are in Svelte. So many of the bad experiences you’ve had with useEffect probably won’t be relevant to Svelte with the $effect rune.

It’s also worth nothing that the section above the return statements runs after the DOM has been updated. So, if you need to access to an effect before the DOM updates, you’re best using the $effect.pre rune.

The $effect.pre rune

The $effect.pre rune, is a regular $effect rune, however it addresses the edge case where you need to run code before the DOM updates.

So in a regular $effect rune, the code runs after the DOM updates.

If you’re coming from old Svelte:

  • The $effect rune replaces the afterUpdate method (which is deprecated in Svelte 5), and
  • The $effect.pre replaces the beforeUpdate method (which is also deprecated in Svelte 5).

Given how much of an edge case this rune addresses, the best example I could give is fairly verbose. Therefore this example is from the Svelte 5 preview documentation.

<script>
	import { tick } from 'svelte';

	let div;
	let messages = [];

	$effect.pre(() => {
		if (!div) return; // not yet mounted
		// reference `messages` so that this code re-runs whenever it changes
		messages; // autoscroll when new messages are added
		if (div.offsetHeight + div.scrollTop > div.scrollHeight - 20) {
			tick().then(() => {
				div.scrollTo(0, div.scrollHeight);
			});
		}
	});
</script>

<div bind:this={div}>
	{#each messages as message}
		<p>{message}</p>
	{/each}
</div>

As we need the code to run before the DOM is updated to provide the correct user experience we want, we’ve used the $effect.pre, in tandem with the tick utility.

The $effect.pre rune, prepares for the DOM to update, and then if the condition is met, the tick waits for the DOM to update with the new messages, and then scrolls to the bottom so that the user sees the latest message.

via GIPHY

Notice that in this example, messages is just placed in the function without any instruction, just so that we can include it as a dependency.

Now lets move onto another edge case rune that might take a little more brain power to understand.

The $effect.active rune

The $effect.active rune tells you whether some piece of code is in an active effect.

Where $effect.active() returns true is where your code is in an active effect, which lets us determine whether there are potential memory leaks, and can be helpful for debugging.

It also allows us to read values without creating subscriptions or memory leaks.

via GIPHY

For example, in your .svelte component, you could have:

<script>
	console.log('This will show false', $effect.active());

	$effect(() => {
		console.log('This will show true', $effect.active());
	});
</script>

<p>This will show true: {$effect.active()}</p>

In the first console.log() where it returns false, this is because the $effect.active() is running during the component setup, and therefore there’s no active effect being run at the time this line of code runs.

For obvious reasons, the second instance we call $effect.active returns true, because it is explicitly inside an $effect rune, so we can clearly visualize that it’s being called within an active effect.

The least obvious instance is when calling $effect.active() inside your markup and have it printing true. This may seem counter-intuitive as you’d assume it’s not part of an effect, however because it’s in your template, you can subscribe to values in your markup, and therefore it’s true.

Now we can look at our final rune.

via GIPHY

Note: This one is also fairly advanced. 🤓

The $effect.root rune

The $effect.root rune is used to create nested effects that you want to control manually, and doesn’t clean up automatically.

Given the these are nested effects, $effect.root also differs from the normal $effect rune in that you can create these effects outside of the component initialization stage.

For example, we might want to turn effects on or off at different stages, so we could do something like this:

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

	const cleanup = $effect.root(() => {
		$effect(() => {
			console.log(points);
		});

		return () => {
			console.log('Clean up the effect root');
		};
	});
</script>

<button onclick={() => cleanup()}>Cleanup</button>

So, by clicking the Cleanup button, we manually get to pick the the effect is cleaned up.

And that’s all of the runes covered! 🥳

via GIPHY

So know we know all of these changes are here, it’s important to know ”why?” from a high level.

Why do we need runes?

There’s a number of reasons why runes are important. Beyond the arguable fact that it makes your code easier and cleaner to write, the main benefits come from:

So let’s look into these aspects a bit further.

Aligned behavior between .svelte components and .js files

If you’ve been developing in Svelte long enough, you would’ve dealt with some of the reactivity quirks. In simple applications, or single-component demo’s, the current reactivity (and associated syntax) is straightforward, and very compelling.

The reactivity also works perfectly 95% of the time, however occasionally there’s issues when you’re handling a let variable in a .svelte file, and it’s either being passed from, or to a .ts file. You see, these different files handle the let keyword differently:

  • in the .svelte file, the variable is reactive, however,
  • in the .ts file, it is not reactive.

A situation like this leads to inconsistencies where variables aren’t reactive in the .ts file, however they ought to be, as something in a .svelte components is dependent on it.

To solve this (and provide some additional state management), Svelte stores would be employed, which would have looked something like this:

src/utils/store.ts

import { writable } from 'svelte/store';

export function createCounter() {
	const { subscribe, update } = writable(0);

	return {
		subscribe,
		increment: () => update((n) => n + 1)
	};
}

However, now because the $state rune is cross-component and reactive, we can make the store like this instead:

src/utils/store.ts

export function createCounter() {
	let count = $state(0);

	return {
		get count() {
			return count;
		},
		increment: () => (count += 1)
	};
}

As the $state keyword works anywhere in your app, you’d be generally better off declaring reactive variables with this, as you can use it in both your .svelte components, and .ts files, with identical expected behaviors.

With runes, most of the reactivity uncertainty is removed, and we have a better grip on the reactivity offered between files.

Runes uses Signals

Under the hood, runes use signals, which isn’t something you’ll need to be familiar with when you’re coding, however it means your application will be much faster.

From a high level, an example where signals improve reactivity is that if you wanted to change one value inside a large list, the other values in the list would not need to be invalidated. In Svelte today, this is not the case.

Conclusion

Congratulations if you studiously made it through all of the runes! Most of them were pretty straightforward, but admittedly things start to get a bit hairy when you go into the depths of the $effect runes.

Alas, this is the future of Svelte. Most of the legacy syntax will remain throughout the rollout of Svelte 5, but it’s a losing battle hanging on to the ways of the past.

As mentioned earlier, runes and a lot of the other Svelte 5 features are a work in progress, so it’s likely we’ll see changes both under that hood, and in the rune API’s.

So until next time, hop into the playground, start up a new Svelte 5 project and get jiggy with runes. ❤️

via GIPHY

FAQ’s

Thanks for reading ❤️

Here are some other articles I think you might like!