Svelte 5 führt neue Runen-API ein

Im November 2023 kündigte das Svelte-Team die Beta zu Svelte 5 an. Die Preview hält neue APIs bereit und ist auf npm verfügbar. Zeit für einen Überblick!

In Pocket speichern vorlesen Druckansicht 43 Kommentare lesen

(Bild: Blackboard/Shutterstock.com)

Lesezeit: 11 Min.
Von
  • Simon Holthausen
Inhaltsverzeichnis

Svelte ist ein UI-Framework, das deklarativen Code wie den folgenden in optimierten JavaScript-Code verwandelt, der das User Interface immer dann updatet, wenn count erhöht wird.

<script>
	let count = 0;

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

<button on:click="{increment}">clicks: {count}</button>

Die Umwandlung funktioniert, da der Compiler "sieht", wo count referenziert wird, und weil er Syntax wie let oder = als Anzeichen dafür interpretiert, dass count ein Zustand ist, der veränderbar ist. Das Resultat: Es braucht weniger Code, da der Compiler sich um das Meiste kümmert.

Leider funktioniert diese schöne deklarative Welt nur auf der obersten Ebene von Komponenten – möchte man eine wiederverwendbare createCounter-Methode extrahieren, muss man stattdessen Svelte's Stores nutzen:

// Definition
import { writable } from 'svelte/store';

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

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

Bei der Verwendung lässt sich dann zwar die schöne Kurzschreibweise für das Subscriben auf Stores nutzen (der $-Prefix), einige Entwicklerinnen und Entwickler finden diesen jedoch ungewöhnlich. Zudem lässt er sich nur innerhalb von .svelte-Dateien verwenden.

<!-- Verwendung -->
<script>
	import { createCounter } from './somewhere.js';

	const count = createCounter();
</script>

<button on:click="{count.increment}">clicks: {$count}</button>

Wäre es nicht schöner, wenn dieselbe ergonomische und elegante Syntax ohne Stores, die von Svelte-Komponenten bekannt ist, überall zum Einsatz kommen könnte?

Svelte 5 ermöglicht genau das durch die Einführung von Runes – zu Deutsch: Runen. Runen sind Buchstaben oder Zeichen, die als magische Symbole genutzt werden – und weil sich das Svelte-Entwicklerteam nicht zu ernst nimmt, wurde die API danach benannt. Nüchterner formuliert sind sie Compiler-Anweisungen. Die einfachste davon ist die $state-Rune, um einen reaktiven Zustand zu definieren:

<script>
	let count = $state(0); // vorher: let count = 0

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

<button on:click="{increment}">clicks: {count}</button>

$state ist hier keine Funktion, sondern eine Anweisung an den Compiler, diese Variable besonders zu behandeln und nach Änderungen an ihrem Zustand das User Interface neu zu rendern. Der Wert, der hineingeben wird, ist der, der auch herauskommt – aus TypeScript-Sicht ist das also die Identitätsfunktion.

Für solch einen einfachen Fall mag das auf den ersten Blick wie ein Rückschritt wirken, da es etwas mehr Code erfordert. Auf den zweiten Blick bietet es jedoch zahlreiche Vorteile: Der Code ist lesbarer, da sich unmittelbar nachvollziehen lässt, welche Variablen einen Zustand ausdrücken, und welche nicht. Vor allem jedoch ermöglichen sie es, den Code viel einfacher zu extrahieren und zu refaktorieren, ohne, dass man auf Sveltes Stores zurückgreifen muss. Eine createCounter-Methode mit Runes sieht dann so aus:

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

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

Die Getter-Methode ist nötig, damit die Reaktivität von count bewahrt bleibt und über die Funktionsgrenzen hinweg transportiert wird. So bekommt jede Stelle, die count ausliest, den aktuellen Wert zugespielt.

Wer lieber Klassen zur Beschreibung solcher Objekte nutzt, kann das ebenfalls tun. Der Code sähe dann stattdessen so aus:

class Counter {
	count = $state(0);
	increment = () => this.count++;
}

Die Verwendung sieht dann folgendermaßen aus:

<script>
	import { createCounter } from './somewhere.svelte.js';
	// oder { Counter } from ...

	const counter = createCounter(); // oder new Counter()
</script>

<button on:click="{counter.increment}">clicks: {counter.count}</button>

Zum Zeitpunkt der Veröffentlichung geht das Svelte-Entwicklerteam der Überlegung nach, Runes per Default nur in .svelte- und .svelte.js/ts-Dateien zu erlauben, damit für Developer und Compiler sofort ersichtlich ist, in welchen Dateien Runes enthalten sein können. Wahrscheinlich wird das jedoch konfigurierbar sein.

Neben $state gibt es noch weitere Runes. Die erste davon ist $derived, die – wie der Name vermuten lässt – abgeleiteten Zustand darstellt:

let count = $state(0);
const double = $derived(count * 2);

$derived ersetzt die Reactive Statements, die vorher so aussahen:

let count = 0;
$: double = count * 2;

Neben der geänderten Syntax hat $derived einen entscheidenden Vorteil bei der Verwendung: Wann immer der Wert ausgelesen wird, ist er aktuell. $: double = count * 2 wurde immer nur direkt vor dem Rendern des UI auf den neuen Wert aktualisiert, was in der Vergangenheit schon oft zu unangenehmen Überraschungen bei Svelte-Nutzerinnen und -Nutzern führte.

Reaktive Statements ließen sich außerdem für Seiteneffekte nutzen:

let count = 0;
$: console.log(`The count is now ${count}`);

Die werden durch die $effect-Rune ersetzt:

let count = $state(0);
$effect(() => console.log(`The count is now ${count}`));

Auch hier gilt: Auf den ersten Blick mag das für erfahrene Svelte-Nutzerinnen und -Nutzer wie ein Rückschritt wirken, doch ist es wesentlich leserlicher – Entwicklerinnen und Entwickler verbringen viel mehr Zeit mit dem Lesen als dem Schreiben von Code – und bietet zusätzliche Vorteile. So kann man bei Bedarf eine Funktion von $effect zurückgeben, die immer dann aufgerufen wird, bevor $effect zum Einsatz kommt oder wenn die Komponente zerstört wird. Der folgende Code wäre in Svelte 4 deutlich aufwendiger zu schreiben:

let count = $state(0);
$effect(() => {
	let seconds = 0;
	const interval = setInterval(
		() => console.log(`The count is ${count} since ${++seconds} seconds`),
		1000
	);
	return () => clearInterval(interval);
});

$effect wird mindestens einmal ausgeführt, und danach immer dann, wenn sich eine seiner Abhängigkeiten ($state oder $derived, der innerhalb des Effekts gelesen wird) geändert hat. Der Effekt wird nach dem Neurendern des User Interface (UI) ausgeführt. Wer stattdessen direkt vor dem Rendern einen Effekt ausführen will, nutzt $effect.pre – die API ist identisch zu $effect, nur das Timing ist anders.

Zu guter Letzt gibt es noch die $props()-Rune, die zur Definition der Properties einer Komponente zum Einsatz kommt. Sie ersetzt die bisherigen export let-Definitionen.

<script>
	let { surname, lastname } = $props();
	// vorher:
	// export let surname;
	// export let lastname;
</script>

<p>Hello {surname} {lastname}</p>

Insgesamt erfordern Runes vorab ein kleines bisschen mehr Denkarbeit ("ist das hier Zustand? Dann muss ich die $state Rune nutzen") und minimal mehr Schreibarbeit. Dafür ist der Code leserlicher, wartbarer, leichter zu refaktorieren, und kompliziertere Patterns sind durch die universelle Reaktivität einfacher möglich. Svelte wird zudem noch einfacher zu erlernen, da sich alles, was besonders im Script-Teil einer Komponente ist, durch Runen ausdrücken lässt.

Runes sind sicherlich die größte Änderung, die uns in Svelte 5 erwartet. Daneben gibt es weitere Änderungen, die darauf abzielen, die Anzahl an Konzepten, die es zu lernen gilt, zu reduzieren und gleichzeitig die Flexibilität des JavaScript-Frameworks zu erhöhen.

Die erste dieser Änderungen sind Event Attributes. Bisher wurden Events in Svelte durch ein on:-Prefix definiert:

<!-- HTML-Element -->
<button on:click={() => ..}>click me</button>
<!-- Komponente -->
<Button on:click={() => ..}>click me</Button>

Zwar lässt das on:-Präfix sofort erkennen, dass es sich um ein Event handelt, jedoch hat diese Lösung einige Nachteile, die sich gerade beim Erstellen von Komponenten zeigen:

  • Events nach oben durchzureichen, ist mühsam. Es ist nicht möglich, Svelte zu sagen "reiche einfach alle Events dieses Elements an meine Verwenderkomponente weiter", stattdessen müssen Entwicklerinnen und Entwickler jedes einzeln explizit weitergeben.
  • Komponenten-Events zu definieren, ist umständlich. Man muss createEventDispatcher nutzen und Events darüber anstoßen. Diese Events sind vom Typ CustomEvent, daher müssen Verwender event.detail schreiben, nur um an den eigentlichen Payload zu kommen.
  • Komponenten-Events lassen sich auch als Callback-Properties definieren, was bedeutet, dass es zwei Wege gibt, die gleiche Sache zu tun.

Event Attributes lösen alle diese Probleme. Statt on:click schreibt man onclick – sprich, alle DOM-Attribute, die mit on beginnen, sind Events. Dadurch lassen sie sich auf der Ebene der Komponenten wie Properties behandeln und entsprechend einfach durchreichen. Das sieht dann etwa so aus:

<script>
	let { onclick, onHello, ...rest } = $props();
</script>

<!-- onclick callback prop an den Button weitergeben -->
<button {onclick}>click me</button>
<!-- alle sonstigen Properties an den Button weitergeben –  alle on-Properties
     sind Events, auf die gehört wird -->
<button {...rest}>click me</button>
<!-- eigene Events per Callback Props aufrufen -->
<button onclick={() => onHello('hello!')}>click me</button>

Wer bereits JSX-Frameworks wie React benutzt hat, wird sich mit Callback-Properties direkt wie zu Hause fühlen. Der einzige Unterschied zu React ist, dass die DOM-Events in Svelte komplett kleingeschrieben werden, also onclick statt onClick.

Eine weitere Neuerung sind Snippets. Sie ermöglichen es, kleine wiederverwendbare UI-Schnipsel zu definieren und an verschiedenen Stellen zu verwenden. Sie sind wie der kleine Bruder einer Komponente: Schneller erstellt, fokussiert auf UI-Wiederverwendung, dafür weniger mächtig. Ein Snippet wird folgendermaßen definiert:

{#snippet snippetWithoutArgument()}
	<p>hello world</p>
{/snippet}
{#snippet snippetWithArgument(name)}
	<p>hello {name}</p>
{/snippet}

Sie lassen sich mithilfe des @render-Tags rendern:

{@render snippetWithoutArgument()}
{@render snippetWithArgument('Svelte')}

Snippets lassen sich nicht nur innerhalb derselben Komponente rendern, in der sie definiert sind, sie lassen sich auch als Properties an eine Komponente weiterreichen:

<!-- Verwender: -->
{#snippet row(item)}
	<span>{item}</span>
{/snippet}

<List list={..} {row} />

<!-- List.svelte: -->
<script>
	let { row, list } = $props();
</script>

<ul>
	{#each list as item}
		<li>{@render row(item)}</li>
	{/each}
</ul>

Wenn das Snippet nur für diese Komponente gedacht ist, kann als Kurzschreibweise das Snippet innerhalb der Komponente definiert werden. Damit wird das Snippet implizit als Property desselben Namens in die Komponente hineingereicht.

<!-- Äquivalent zu obigem Code: -->
<List list={..}>
	{#snippet row(item)}
		<span>{item}</span>
	{/snippet}
</List>

Somit sind Snippets also nicht nur ein neues Feature zum Definieren wiederverwendbarer UI-Elemente, sie agieren zugleich als bessere Version der Slot-API. Statt let:x={y}[code], [code]<slot name="..">, <svelte:fragment>, $$slots und vielen weiteren umständlichen Bestandteilen der Slot-API kommen nun Snippets zum Einsatz, die leichter zu erlernen und zu verstehen sind und zugleich mehr Flexibilität und Möglichkeiten bieten.

Runes, Event Attributes und Snippets sind bedeutende Änderungen an der Svelte-API. Nicht jedes Projekt kann jedoch in einem Rutsch auf die neue Syntax umstellen. Daher ist das Svelte-Team sehr darauf bedacht, das Upgrade auf Svelte 5 so reibungslos wie möglich zu gestalten. Konkret bedeutet das: Die komplette API, die wir aus Svelte 4 kennen, funktioniert auch in Svelte 5. Event Attributes lassen sich Seite an Seite mit den bisherigen Events verwenden, ebenso Slots und Snippets. Lediglich Runes sind pro Komponente exklusiv: Wer eine Rune benutzt, kann die alte Syntax wie etwa $: und export let nicht mehr in dieser Komponente nutzen.

Komponenten, die Runes nutzen, können jedoch ohne Problem Komponenten verwenden, die noch keine Runes nutzen, und umgekehrt. Das ist besonders hilfreich, da es wahrscheinlich etwas Zeit braucht, bis alle Svelte-Libraries auf die neue Syntax umgestellt sind. Das Svelte-Entwicklerteam hofft, dass Entwicklerinnen und Entwickler dann für die meisten Apps nicht mehr als eine Handvoll Zeilen Code ändern müssen, wenn sie auf die neue Version upgraden.

Momentan plant das Svelte-Team, die alten APIs in Svelte 5 und 6 weiterhin zu unterstützen und in Svelte 7 zu entfernen. Das sollte allen Projekten genügend Zeit geben, sich anzupassen.

Svelte erfindet sich mit Version 5 ein Stück weit neu. Das Maintainer-Team hat aus den Erfahrungen und dem Feedback der Vergangenheit seine Schlüsse gezogen und geht mit der Runes-API neue Wege. Event Attributes und Snippets sind ebenfalls vielversprechende Änderungen, die Entwicklerinnen und Entwicklern neue Möglichkeiten zum Schreiben ihres Codes für Apps und Libraries eröffnen. Obwohl die Änderungen zahlreich sind, bleibt die neue Version des UI-Frameworks dem Grundgedanken treu: Es ist weiterhin schlank und legt den Fokus auf Entwicklerzufriedenheit. Gleichzeitig wird es für Neulinge noch einfacher zu erlernen – Events und Snippets sind nun einfach weitere Arten von Komponenten-Properties, wodurch sich die Zahl der zu lernenden Konzepte reduziert.

Ein Veröffentlichungsdatum haben die Maintainer noch nicht genannt, sicher ist jedoch, dass Svelte 5 im Laufe des Jahres erscheinen wird. Bis dahin können Neugierige den Preview-Playground nutzen, um Svelte 5 im Browser auszuprobieren, oder die Vorabversion auf npm installieren und lokal nutzen.

Simon Holthausen
ist Softwareentwickler bei Vercel. Dort arbeitet er als Teil des Maintainer-Teams Vollzeit am Frontend-Framework Svelte.

(mdo)