Eine der häufigsten Fragen rund um Actions: „Was passiert, wenn sich der Parameter zur Laufzeit ändert?" Die Antwort ist nicht ganz selbsterklärend — denn die Action-Funktion selbst läuft nur einmal beim Mount. Dieser Artikel zeigt, wie du trotzdem auf Änderungen reagierst, was der Unterschied zwischen der klassischen update-Methode und der modernen $effect-Variante ist und welche Stolperfalle vielen Entwicklern unterläuft.
Was passiert, wenn sich der Parameter ändert?
Stell dir vor, du hast eine Tooltip-Action, deren Text sich ändern können soll:
<script>
import { tooltip } from '$lib/actions/tooltip.svelte';
let label = $state('Hallo');
</script>
<button use:tooltip={{ text: label }}>Hover</button>
<input bind:value={label} />Wenn der Nutzer im Eingabefeld tippt, ändert sich label. Die Frage ist: Bekommt die Tooltip-Action diese Änderung mit?
Die ehrliche Antwort: Es kommt darauf an, wie die Action geschrieben ist. Es gibt zwei Wege.
Weg 1 – Klassische update-Methode
In der älteren Schreibweise gibst du aus der Action ein Objekt mit einer update-Methode zurück. Svelte ruft diese Methode jedes Mal auf, wenn sich der use:-Parameter ändert.
export function tooltip(node: HTMLElement, params: { text: string }) {
let currentText = params.text;
const handler = () => {
console.log('Tooltip:', currentText);
};
node.addEventListener('mouseenter', handler);
return {
update(newParams: { text: string }) {
currentText = newParams.text;
},
destroy() {
node.removeEventListener('mouseenter', handler);
},
};
}Verhalten:
- Setup läuft einmal beim Mount mit
params. - Bei jeder Änderung von
labelruft Svelteupdate({ text: neuerWert })auf. - Beim Unmount ruft Svelte
destroy().
Diese Schreibweise ist solide und funktioniert seit Svelte 3.
Weg 2 – Moderne Variante mit $effect
In der Runes-Welt schreibt man dasselbe Verhalten kompakter — und mit klarer reaktiver Semantik:
import type { Action } from 'svelte/action';
export const tooltip: Action<HTMLElement, { text: string }> = (node, params) => {
$effect(() => {
const text = params.text; // Tracking
const handler = () => console.log('Tooltip:', text);
node.addEventListener('mouseenter', handler);
return () => node.removeEventListener('mouseenter', handler);
});
};Was hier passiert:
$effectläuft beim ersten Mount mit dem aktuellen Parameter.- Sobald
params.textsich ändert, wird der Cleanup ausgeführt (Listener entfernt) und der Effect läuft neu. - Beim Unmount läuft der Cleanup ein letztes Mal.
Vorteil: Kein separates update-Konstrukt. Reaktivität funktioniert hier genauso wie überall sonst in Runes.
Die wichtigste Stolperfalle: nicht-reaktive Parameter
Achtung — die Action wird immer nur einmal aufgerufen. Wenn du den Parameter nur außerhalb eines $effect liest, bekommst du keine Reaktivität:
export function tooltip(node: HTMLElement, params: { text: string }) {
// params.text wird einmalig beim Mount gelesen
const handler = () => console.log('Tooltip:', params.text);
node.addEventListener('mouseenter', handler);
}Das sieht erst mal richtig aus — aber: Wenn params.text sich später ändert, weiß der Handler nichts davon. Er hat den Wert von damals geschlossen.
Wichtig: Das Parameter-Objekt selbst ist in Svelte 5 reaktiv. Wenn du es innerhalb von $effect liest, wird die Reaktivität verfolgt:
export function tooltip(node: HTMLElement, params: { text: string }) {
$effect(() => {
const text = params.text; // <-- hier wird getrackt
const handler = () => console.log('Tooltip:', text);
node.addEventListener('mouseenter', handler);
return () => node.removeEventListener('mouseenter', handler);
});
}Zugriff auf params.text innerhalb des $effect löst beim Wechsel ein erneutes Auführen aus.
Mehrere reaktive Parameter
Wenn dein Action-Parameter mehrere Felder hat, kannst du sie entweder gemeinsam oder einzeln tracken — je nachdem, was du brauchst.
export function tooltip(node: HTMLElement, params: { text: string; placement: 'top' | 'bottom' }) {
// Effect, der nur auf text reagiert
$effect(() => {
node.dataset.tooltipText = params.text;
});
// Effect, der nur auf placement reagiert
$effect(() => {
node.dataset.tooltipPlacement = params.placement;
});
}Jeder $effect verfolgt automatisch nur die darin gelesenen Werte. Eine Änderung an text löst nur den ersten Effect aus, eine Änderung an placement nur den zweiten.
Wenn der Parameter teure Operationen auslöst
Manchmal soll bei Parameter-Wechsel nur ein Wert aktualisiert werden, ohne den ganzen Setup neu zu machen. Beispiel: Eine Library-Instanz, die teuer zu erzeugen ist, aber eine günstige setOption()-Methode anbietet.
export function chart(node: HTMLElement, params: { data: number[] }) {
// Setup läuft einmal, ohne Tracking
const instance = new ChartLib(node);
$effect(() => {
// Nur Daten reaktiv aktualisieren
instance.setData(params.data);
});
// Unmount-Cleanup
$effect(() => {
return () => instance.destroy();
});
}Hier wird die Library-Instanz nur einmal erzeugt. Bei jeder Datenänderung wird nur die günstige setData-Methode aufgerufen — kein vollständiges Re-Setup.
Vergleich: update vs. $effect
| Aspekt | update-Methode (klassisch) | $effect (modern) |
|---|---|---|
| Schreibweise | Objekt mit update/destroy | Eine Funktion mit $effect |
| Reaktivität auf einzelne Felder | Manuell vergleichen | Automatisch granular |
| Cleanup | Im destroy | Rückgabefunktion in $effect |
Funktioniert in .ts | Ja | Nur in .svelte.ts/.svelte |
| Lesbarkeit bei mehreren Effects | Schwierig (alles in update) | Ein $effect pro Verantwortung |
| Performance bei häufigen Updates | Manuelle Optimierung möglich | Granular automatisch |
| Empfehlung Svelte 5 | Bestandscode behalten | Neuer Code |
Besonderheiten
Parameter außerhalb von $effect lesen.
Klassischer Fehler: Du liest params.text direkt im Action-Body, nicht im $effect. Reaktivität funktioniert dann nicht.
Closure schließt veraltete Parameter ein.
Ein Listener, der einmalig beim Mount registriert wird, schließt den damaligen Parameter-Wert ein. Wenn der Parameter sich ändert, sieht der Listener weiterhin den alten Wert. Lösung: Listener im $effect registrieren, mit Cleanup.
Cleanup vergessen.
Wer in $effect Listener oder Observer registriert, ohne Cleanup, hat bei jedem Re-Run Memory-Leaks.
update-Methode mit komplexer Logik.
Wenn update zu groß wird, lohnt der Wechsel auf $effect-basierte Form — automatisches Tracking ist meist klarer.
Action ohne Parameter, aber Aufruf mit {}.
use:meineAction ohne Parameter ist okay. use:meineAction={{}} ist auch okay. Aber wenn die Action keinen zweiten Parameter erwartet, hat das Übergeben keinen Effekt — und kann verwirrend sein.