Svelte Headless CMS

Svelte 5 Runes explained (for Svelte 4 developers)

Blog Getting Started

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.

Key Features of Runes

  1. Explicit Dependencies: Runes allow developers to clearly define which variables depend on others, making the flow of data and updates more explicit.
  2. Fine-Grained Reactivity: By using Runes, you can create very specific reactive statements, which can lead to more optimized and efficient updates.
  3. Declarative Syntax: Runes use a syntax that is similar to JavaScript, but with special characters and keywords that indicate reactive behavior.

The $state Rune

Wrap 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.

The $props Rune

This replaces export let ... in sub-components:

<script>
  // Old
  export let title;
  export let amount = 0;
  // New
  let { title, amount = 0 } = $props();
</script>

$bindable

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} />

The $derived Rune

This replaces $: for example:

<script>
    let count = 0;
    let doubleCount = $derived(count * 2);

    function increment() {
        count += 1;
    }
</script>

<p>{doubleCount}</p>

$derived.by()

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>

Comparison with Previous Versions

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>

The $effect Rune

As 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);
});

Comparison with Previous Versions

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

Snippets

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.

Slots

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()}

Related docs


Download the code for this blog from GitHub at https://github.com/webuildsociety/svelte-headless