Eine Action ist im einfachsten Fall eine Funktion. Aber je nach Anforderung sieht ihr innerer Aufbau unterschiedlich aus — vom kleinen Mount-Listener bis hin zur reaktiv parametrisierten DOM-Erweiterung. Dieser Artikel zeigt die drei wichtigsten Schreibweisen, wann du welche nimmst und wie du Actions in TypeScript-Projekten korrekt typisierst.
Form 1 – Moderne Variante mit $effect
Die heute empfohlene Form: Setup und Cleanup laufen in einem $effect innerhalb der Action.
export function autoFocus(node: HTMLElement) {
$effect(() => {
node.focus();
});
}Was passiert hier:
- Die Action ist eine normale Funktion, die den DOM-Knoten als Argument bekommt.
- Innerhalb der Action öffnet
$effecteinen Reactive-Scope, der mit dem Element-Lifecycle gebunden ist. - Wenn ein Cleanup nötig ist (z. B. Listener entfernen), kannst du eine Funktion aus dem
$effectzurückgeben.
export function tooltip(node: HTMLElement, text: string) {
$effect(() => {
const handler = () => console.log(text);
node.addEventListener('mouseenter', handler);
return () => node.removeEventListener('mouseenter', handler);
});
}Diese Form hat zwei wichtige Eigenschaften:
- Setup und Cleanup stehen eng beieinander (gleicher
$effect-Block) — keine Verteilung über die ganze Funktion. - Wenn die Action Parameter empfängt, die sich ändern können, kann der
$effectdarauf reagieren (siehe Action-Parameter).
Form 2 – Klassisches Pattern mit update und destroy
Vor der Runes-Ära war das Standard: Die Action gibt ein Objekt mit optionalen update- und destroy-Methoden zurück.
export function tooltip(node: HTMLElement, text: string) {
let currentText = text;
function show() {
console.log('Tooltip:', currentText);
}
node.addEventListener('mouseenter', show);
return {
update(newText: string) {
currentText = newText;
},
destroy() {
node.removeEventListener('mouseenter', show);
},
};
}So funktioniert das:
update(newParam)wird von Svelte aufgerufen, wenn sich deruse:-Parameter ändert.destroy()wird beim Unmount aufgerufen.
Diese Form funktioniert in Svelte 5 weiterhin, ist aber etwas länger und hat keinen Reactive-Scope. Für neue Actions ist Form 1 (mit $effect) idiomatischer.
Form 3 – Inline-Attachments
Seit Svelte 5.29 gibt es eine neue Variante: das {@attach fn}-Tag. Statt eine Action über use: an ein Element zu hängen, hängst du sie direkt im Markup als Attachment ein:
<script>
function focus(node) {
$effect(() => {
node.focus();
});
}
</script>
<input {@attach focus} />Vorteile von Attachments:
- Erlaubt inline-definierte Logik direkt im Markup.
- Funktioniert mit höher-ordentlichen Funktionen, ohne Boilerplate.
- Reagiert besser auf reaktive Änderungen.
Wann nimmst du was?
| Situation | Empfehlung |
|---|---|
| Wiederverwendbares Verhalten als externes Modul | use: Action |
| Einmal-Logik direkt im Markup | {@attach} |
| Bestandscode aus Svelte 4 | use: Action |
Library-API mit Action-Type | use: Action |
Beide existieren parallel und werden weiter unterstützt. Wer in einem neuen Projekt startet, kann mit Attachments oft kompakter arbeiten — wer eine wiederverwendbare Library aufbaut, bleibt bei Actions.
TypeScript: der Action-Type
Aus dem Modul svelte/action kommt ein generischer Helfer, mit dem sich Actions sauber typisieren lassen:
import type { Action } from 'svelte/action';
type TooltipParams = { text: string; placement?: 'top' | 'bottom' };
export const tooltip: Action<HTMLElement, TooltipParams> = (node, params) => {
$effect(() => {
const handler = () => console.log(params.text);
node.addEventListener('mouseenter', handler);
return () => node.removeEventListener('mouseenter', handler);
});
};Der Type hat drei Generics:
NodeType— auf welchen Elementtyp passt die Action (z. B.HTMLElement,HTMLInputElement,HTMLDivElement).ParameterType— wie das Parameter-Objekt aussieht.Events— selten genutzt; zum Deklarieren von Custom-DOM-Events, die die Action auf dem Element auslöst.
In den meisten Fällen reichen die ersten beiden Generics.
Wenn keine Parameter
import type { Action } from 'svelte/action';
export const autoFocus: Action<HTMLElement> = (node) => {
$effect(() => node.focus());
};Element-spezifische Action
import type { Action } from 'svelte/action';
export const selectOnFocus: Action<HTMLInputElement> = (node) => {
$effect(() => {
const handler = () => node.select();
node.addEventListener('focus', handler);
return () => node.removeEventListener('focus', handler);
});
};<button use:selectOnFocus> würde der TypeScript-Compiler als Fehler markieren — die Action akzeptiert nur <input>-Elemente.
Aufbau einer typischen Action
Ein bewährtes Schema für Actions, die mehrfach Listener oder Observer setzen:
import type { Action } from 'svelte/action';
type Params = {
// ...Parameter-Felder
};
export const meineAction: Action<HTMLElement, Params> = (node, params) => {
// 1. Optional: Initial-Setup, das nicht reaktiv sein soll
// (DOM-Attribute setzen, Klassen ergänzen)
// 2. Reaktiver Bereich
$effect(() => {
// Setup mit aktuellen params
const cleanup = () => {
// Listener/Observer entfernen
};
// ...Listener registrieren
return cleanup;
});
};Wenn eine Action keine reaktiven Parameter hat, reicht oft auch ein Top-Level-$effect. Wenn sie mit Parametern arbeitet, die sich ändern können, ist diese Struktur die saubere Wahl.
Wann .svelte.ts für Actions?
Damit $effect innerhalb einer Action funktioniert, muss die Datei runes-fähig sein. Das heißt:
- In einer
.svelte-Datei (z. B. lokal definierte Action) — funktioniert. - In einer
.svelte.ts/.svelte.js-Datei — funktioniert. - In einer normalen
.ts/.js-Datei —$effectist nicht verfügbar; dann nur die klassische Form mitupdate/destroymöglich.
Empfehlung: Actions, die $effect nutzen, gehören in .svelte.ts-Dateien.
Häufige Stolperfallen
$effect in einer normalen .ts-Datei.
Dann bleibt nur die klassische Form mit update/destroy. Oder Datei umbenennen auf .svelte.ts.
Klassische Form mit reaktiven Parametern erwartet.
update(newParam) wird zwar bei Parameter-Wechseln aufgerufen, ist aber nicht reactive im Runes-Sinn. Wer interne $state-Werte aus dem Parameter ableiten will, fährt mit der $effect-Variante besser.
Cleanup vergessen.
Egal welche Form: Wer Listener registriert und nicht entfernt, leckt sie. In $effect ist Cleanup eine Rückgabefunktion, in der klassischen Form eine destroy-Methode.
Action für Komponenten verwenden wollen.
use: funktioniert nur an HTML-Elementen, nicht an Komponenten-Tags. Wer Verhalten an eine Komponente hängen will, gibt es als Prop weiter oder kapselt die Logik in der Komponente selbst.
Mehrere $effect ohne Trennung.
Wenn dein Code mehrere unabhängige Listener registriert, mach mehrere $effect-Blöcke daraus — jeder mit eigener Aufgabe. Das hält Cleanup übersichtlich.