Svelte 5 introduces a new feature called "Runes" that changes how Svelte developers manage reactivity. Runes are essentially a way to explicitely say what is reactive, rather than "everything" potentially being reactive in Svelte 4, which leaves it to the compiler to decide.
As Svelte 4 projects grow the compiler doesn't always get it right and things can slow down. In 5, developers explicitly tag variables as changeable - and causing change instead.
$state
RuneWrap the function $state()
around the value part of your declaration e.g.:
<script>
// Wrap your "0" default value with the $state() function call
let count = $state(0);
</script>
<button on:click={() => count++}>
clicks: {count}
</button>
In this example, count
can be punched out into the HTML in the normal Svelte way.
Annoyingly yes, this is more verbose that Svelte 4 but it scales better since the compiler doesn't have to try to figure out what you want to be reactive.
The real bonus however is arrays and objects are now deeply reactive - no more settings a variable to itself! e.g.
let numbers = $state([1, 2, 3]);
is all you need.
$props
RuneThis replaces export let ...
in sub-components:
<script>
// Old
export let title;
export let amount = 0;
// New
let { title, amount = 0 } = $props();
</script>
Annoyingly, to use bind and make the values updateable by the component you have to be explicit inside the component e.g.:
<script>
let { title, amount = $bindable(0) } = $props();
</script>
<!-- Passing syntax is the same though -->
<SubComponent title="Hi there" amount=bind:{amount} />
$derived
RuneThis replaces $:
for example:
<script>
let count = 0;
let doubleCount = $derived(count * 2);
function increment() {
count += 1;
}
</script>
<p>{doubleCount}</p>
To do something more sophisticated than a single calculation, use $derived.by(() => {})
to effectively assign a function to recalculate something's value e.g.:
<script>
let numbers = $state([1, 2, 3]);
let total = $derived.by(() => {
let total = 0;
for (const n of numbers) {
total += n;
}
return total;
});
</script>
In previous versions of Svelte, reactivity was often implied by assignments or by using $:
labels for reactive statements. For example, the above example without Runes would use:
<script>
let count = 0;
$: doubleCount = count * 2;
function increment() {
count += 1;
}
</script>
<p>{doubleCount}</p>
$effect
RuneAs a last resort (like when you need to set multiple variables at once) $effect
can be used.
The $effect
directive lets you run your own function whenever Svelte "ticks" (any time a reactive variable's value is changed). It is recommended to only be used if nothing else works because it fires for ANY change and cannot be targetted to specific variables. Here’s an example:
let count = $state(0);
$effect(() => {
// This runs:
// 1. on mount
// 2. whenever any of the state() or derived() or props() variables mentioned internally change
console.log("Count changed to " + count);
});
In previous versions of Svelte, $: if ()
statements could be used to cause effects.
<script>
let count = 0;
let oldCount = 0;
$: if (count > oldCount) { console.log(`Count changed to ${count}`); oldCount = count; }
</script>
Nice, right? This is super-useful for console logging whilst debugging, but most functionality can be achieved using derived or derived.by
The final Svelte 5 change other than runes is Snippets - functions that return a chunk of Svelte (or HTML)
{#snippet someFunctionThatReturnsSvelte(img)}
<img src={img.src} alt={img.caption} width={img.width} height={img.height} />
{/snippet}
{#each images as img}
{#if img.href}
<a href={img.href}>
{@render someFunctionThatReturnsSvelteHTML(img)}
</a>
{:else}
{@render someFunctionThatReturnsSvelteHTML(img)}
{/if}
{/each}
This achieves the same effect as creating a sub-component, but is just a little quicker and keeps the number of files down. It's kind of like JSX in React too, which will probably make those bods happy.
In your +layout.svelte
files (and possibly elsewhere) you will currently have:
<slot />
That's the hole through which all the other child pages content is slotted.
Now you need to use the special snippet called children()
, so that simple +layout.svelte
becomes:
<script lang="ts">
let { children } = $props();
</script>
{@render children()}
Download the code for this blog from GitHub at https://github.com/webuildsociety/svelte-headless