Reactive magic in Svelte 5: Understanding Runes

Here's a preview of Runes—a big new idea for reactive coding and the most important change to Svelte in years.

Reactive magic in Svelte 5: Understanding runes
STEKLO/Shutterstock

Svelte 5 brings improvements under the hood—namely functional components and the adoption of signals—but is otherwise a mostly incremental update. The one exception is the new Runes feature, which introduces a host of ideas for dealing with reactivity in a more modular, succinct, and fine-grained way. 

In this article, you'll get a hands-on introduction to the main runes shipping with Svelte 5: $state(), $derived(), $props(), $inspectI(), and $effect().

Runes in Svelte

At first glance, it might seem that the new runes feature adds complexity to working with Svelte. In fact, this idea offers a simpler approach to doing many things you likely already do. The term rune refers to a magical glyph, or an alphabetic letter with mysterious powers. In Svelte, runes are special tokens that tell the Svelte compiler to work behind the scenes in specific ways to make things happen. 

A rune gives you a simple syntax for telling the Svelte engine to do specific, useful work like managing state and exposing component properties.

The main runes introduced in Svelte 5 are:

  • $state()
  • $derived()
  • $effect()
  • $props()
  • $inspect()

As you can see, a rune is a function prefixed with a dollar-sign character. As the developer, you use these special functions almost exactly like you would any other function. The Svelte engine then takes care of implementing the rune's intended action for you behind the scenes.

$state()

Let’s begin by looking at $state(), which is the rune you will likely use most. In a (very) loose sense, the $state rune does something logically similar to the React useState() hook, providing a functional way to deal with reactive state.

Let’s consider a simple example. Here’s how you’d create an input and display its value in Svelte 4, without runes:


<script>
  let text = "Default";
</script>

<input type="text" bind:value={text}/>
Text: {text}

And now, here is the same action with the $state rune:


<script>
	let text = $state("Default")
</script>

<input type="text" bind:value={text}/>
Text: {text}

In both of these samples, we have a simple state variable (text) and use it to drive a text input that outputs to the screen. This is typical Svelte code, especially the bind:value={text} syntax, which gives you a simple way to two-way bind to an input.

The only change here is that, instead of declaring a normal variable with let text = "Default", we declare it as a state rune: let text = $state("Default"). The "Default" we pass into $state is the initial value.

Notice that the bind:value call doesn’t have to change at all: Svelte knows how to use a rune in that context. In general, the state rune reference acts properly anywhere a variable would work.

Although this is a small change, there is an obvious benefit in clarity. It’s neat that the Svelte compiler magically realizes that let count = 0 should be reactive when it’s at the top of a component. But as the codebase grows, that feature is a bit obfuscating, as it gets hard to tell which variables are reactive. The $state rune eliminates that issue.

Another benefit is that $state can appear anywhere, not just the top level of your components. So, let’s say we wanted a factory function that would create text states for use in arbitrary inputs. Here’s a simple example:


<script>
	let makeText = function(def){
		let myText = $state(def);
		return { 
			get text() { return myText },
			set text(text) { myText = text },
		}
	}
	let text = makeText("test");
</script>

<input type="text" bind:value={text.text}/>
Text: {text.text}

While the example is contrived, the point is the $state() declaration creates a functioning reactive state from inside a different scope—something that requires contortions in the old Svelte syntax. Also notice that in this case, we provided both a getter and a setter for the text variable; this is because the bind:value call is a two-way binding requiring both read and write access to the state object.

Another interesting property of the $state() rune is that it is automatically wired to members of an object:


<script>
	let valueObject = new class { 
		text = $state('I am a test')
		num = $state(42)
	};
</script>

<input type="text" bind:value={valueObject.text}/>
<input type="number" bind:value={valueObject.num}/>
<br>
Text: {valueObject.text}
<br>
Number: {valueObject.num}

The essence of this snippet is that the text and num properties of the valueObject class are automatically bound properly to the inputs, without explicitly declaring the getters and setters. Svelte automatically provides the getters and setters the object needs to access the properties of the valueObject class.

$derived()

In the past, you could create a derived property using the $: syntax in Svelte. This had some limitations, including that values could get stale because the engine only updated the computed value when the component updated. Svelte 5 replaces the $: syntax with $derived(), which keeps the computed value in sync at all times.  

Here’s an example of using $derived to combine strings from text inputs:


<script>
	let greeting = $state("Hello there");
	let name = $state("User");
	let sentence = $derived(greeting + " " + name);
</script>

<input type="text" bind:value={greeting}/>
<input type="text" bind:value={name}/>
<br>
Text: {sentence }

What we are doing here is using the sentence variable as a derived rune. It is derived from the greeting and name state runes. So, a derived rune combines the states of state variables.

Using $derived(greeting + “ “ + name) ensures that whenever the greeting or name changes, the sentence variable will reflect those changes.

$effect()

$effect is a rune that works similarly to React’s useState() effect. It is used to cause effects outside of the reactive engine. Here's an example from the Svelte docs:


$effect(() => {
  // runs when the component is mounted, and again
  // whenever `count` or `doubled` change,
  // after the DOM has been updated
  console.log({ count, doubled });

  return () => {
   // if a callback is provided, it will run
   // a) immediately before the effect re-runs
   // b) when the component is destroyed
	console.log('cleanup');
  };
});

The purpose of this code is to run logging when the component is first mounted, and then whenever the dependent variables count and doubled are modified. The optional return value lets you do any necessary cleanup before the effect runs or when the component is unmounted.

$props()

$props() is the new way to declare and consume component properties in Svelte. This covers a few use cases, especially exporting variables at the top level of components with let. An example is worth a thousand words, and in general the new $props syntax is clean and obvious:


// main.svelte
<script>
  import Component2 from './Component2.svelte';
</script>
<Component2>
</Component2>
<Component2 prop2 = "test">
</Component2>

// Component2.svelte
<script>
let { prop1 = "foo", prop2} = $props();
</script>
{prop1}
<br>
{prop2}

//outputs:
foo
foo
test

Here, the main.svelte component is importing Component2 and demonstrating how to modify the props via properties on the markup. Notice that Component2 can declare default values like prop1 = “foo”.

$inspect()

The last rune we'll look at is $inspect. This is a kind of reactive console logging statement:


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

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

<button onclick={() => count++}>Increment</button>
<input bind:value={message} />

In this example (taken from the Svelte docs), the purpose is to emit a logging statement whenever the count of message variables changes. In essence, it gives you a simple way to log to the console reactively, in response to variable updates.

Conclusion

The overall effect of runes is to simplify the Svelte API for developers. It will take some time to adjust to the new syntax and migrate existing code, but in general, the new approach really is easier. If there is an exception, it's the $effect() rune, which requires a bit more thought before being used to replace existing approaches. The elegance of $state(), $derived(), and $props() more than make up for $effect()'s complexity. All in all the, new Runes feature is a fresh and welcome idea in reactivity.

Copyright © 2024 IDG Communications, Inc.