Werte beobachten und auf Änderungen reagieren
Templates reagieren automatisch auf State-Änderungen – aber was ist mit Seiteneffekten außerhalb des DOM? Wenn eine URL-Parameter aktualisiert, eine API aufgerufen oder ein localStorage-Wert synchronisiert werden soll, braucht Alpine.js einen expliziten Watcher. $watch ist das Werkzeug dafür.
Inhaltsverzeichnis
- 1. $watch: Grundlagen und Syntax
- 2. Seiteneffekte: URL, localStorage, DOM-APIs
- 3. Deep Watch: Objekte und Arrays beobachten
- 4. Debounce und Throttle mit $watch
- 5. Store-Watcher: Globale State-Änderungen beobachten
- 6. x-effect vs. $watch: Wann was verwenden?
- 7. Watcher aufräumen und Speicherlecks vermeiden
- 8. Praxispatterns: Suche, Filter, Formularvalidierung
- 9. $watch im Vergleich: Alpine vs. Vue vs. React
- 10. Zusammenfassung
- 11. FAQ
1. $watch: Grundlagen und Syntax
Alpine.js aktualisiert den DOM automatisch, wenn sich reaktiver State ändert – das ist das Kernversprechen des Frameworks. Aber was passiert, wenn eine Änderung nicht den DOM betrifft, sondern einen Seiteneffekt außerhalb des DOM auslösen soll? Zum Beispiel: Wenn sich der Such-Query ändert, soll eine API aufgerufen werden. Wenn sich eine Filterauswahl ändert, soll die URL aktualisiert werden. Wenn sich ein Formularfeld ändert, sollen weitere Felder validiert werden. Für diese Fälle gibt es $watch.
$watch('propertyName', callback) registriert einen Callback, der aufgerufen wird, wenn der angegebene Wert sich ändert. Der Callback erhält den neuen Wert als erstes Argument und den alten Wert als zweites. $watch wird typischerweise im init()-Hook einer Komponente aufgerufen, wo alle Watcher für die Komponentenlebenszeit registriert werden. Im Gegensatz zu x-effect beobachtet $watch explizit einen benannten Wert, nicht alle Zugriffe innerhalb einer Funktion. Das macht das Verhalten vorhersagbarer und Debugging einfacher.
document.addEventListener('alpine:init', () => {
Alpine.data('searchComponent', () => ({
query: '',
results: [],
loading: false,
lastQuery: '',
init() {
// $watch('propertyName', (newValue, oldValue) => { ... })
this.$watch('query', (newVal, oldVal) => {
console.log(`Query changed: "${oldVal}" -> "${newVal}"`);
this.search(newVal);
});
// Mehrere Watcher gleichzeitig registrieren
this.$watch('results', (results) => {
// Immer wenn Ergebnisse sich ändern, DOM-Titel aktualisieren
document.title = results.length > 0
? `${results.length} Ergebnisse für "${this.query}"`
: 'Suche — Mironsoft';
});
},
async search(query) {
if (query.trim().length < 2) {
this.results = [];
return;
}
this.loading = true;
try {
const res = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
this.results = await res.json();
} finally {
this.loading = false;
}
}
}));
});
Ein wichtiger Unterschied zu Vue.js: $watch in Alpine.js löst den Callback nicht beim Initialisieren aus – nur bei echten Änderungen. Wenn der initiale Wert verarbeitet werden soll, muss das im init()-Hook explizit aufgerufen werden. Das ist oft das gewünschte Verhalten, kann aber unerwartet sein, wenn man von Vue.js kommt, wo Watcher mit { immediate: true } sofort ausgelöst werden können.
2. Seiteneffekte: URL, localStorage, DOM-APIs
Seiteneffekte sind Operationen, die außerhalb des reaktiven Alpine.js-State-Systems stattfinden: URL-Manipulation über die History API, Schreiben in den localStorage, Aufrufen von DOM-APIs wie document.title, Auslösen von CustomEvents oder Schreiben in externe Systeme. $watch ist der natürliche Ort, diese Seiteneffekte zu registrieren, weil sie explizit an einen bestimmten State-Wert gebunden sind. Das macht den Code nachvollziehbar: Wer wissen will, was passiert, wenn sich filters ändert, schaut in den $watch('filters', ...)-Callback.
document.addEventListener('alpine:init', () => {
Alpine.data('catalogFilter', () => ({
filters: {
category: '',
priceMin: 0,
priceMax: 1000,
inStock: false,
sort: 'relevance'
},
products: [],
init() {
// URL-State aus query params lesen beim Init
const params = new URLSearchParams(window.location.search);
if (params.has('category')) this.filters.category = params.get('category');
if (params.has('sort')) this.filters.sort = params.get('sort');
// Filter-Änderungen → URL aktualisieren (History API)
this.$watch('filters', (newFilters) => {
const params = new URLSearchParams();
Object.entries(newFilters).forEach(([key, val]) => {
if (val !== '' && val !== false && val !== 0) {
params.set(key, val);
}
});
const newUrl = `${window.location.pathname}?${params.toString()}`;
window.history.pushState({ filters: newFilters }, '', newUrl);
// Produkte neu laden
this.loadProducts(newFilters);
});
// Analytics: Seiteneffekt auf Kategorie-Änderung
this.$watch('filters.category', (category) => {
if (category && window.dataLayer) {
window.dataLayer.push({
event: 'category_filter',
category_name: category
});
}
});
},
async loadProducts(filters) {
const params = new URLSearchParams(filters);
const res = await fetch(`/api/products?${params}`);
this.products = await res.json();
}
}));
});
3. Deep Watch: Objekte und Arrays beobachten
Standardmäßig verwendet $watch einen flachen Vergleich: Es reagiert auf die Zuweisung eines neuen Objekts, nicht auf die Änderung einer Eigenschaft innerhalb des Objekts. Das bedeutet: Wenn this.filters.category = 'shoes' ausgeführt wird, löst $watch('filters', ...) normalerweise nicht aus, weil filters selbst dasselbe Objekt bleibt. Alpine.js 3 löst dieses Problem durch Deep Reactivity: Im Gegensatz zu manchen anderen Frameworks behandelt Alpine.js verschachtelte Objekte automatisch reaktiv, sodass auch tiefe Änderungen den Watcher auslösen.
Allerdings gibt es einen Unterschied im Callback: Bei tiefen Änderungen erhalten neue und alte Wert im Callback denselben Objektverweis, weil das Objekt mutiert statt ersetzt wurde. Wer den alten Wert für einen Vergleich braucht, muss ihn manuell über JSON.parse(JSON.stringify(value)) vor der Änderung klonen. Für Arrays gilt dasselbe: Methoden wie push(), pop() und splice() lösen den Watcher aus, aber Old und New Value zeigen auf dasselbe Array.
4. Debounce und Throttle mit $watch
Wenn ein Watcher bei schnell aufeinanderfolgenden Änderungen ausgelöst wird – etwa bei Tastatureingaben in ein Suchfeld – soll die Callback-Funktion nicht bei jedem Tastendruck ausgeführt werden. Debounce verzögert den Aufruf, bis eine bestimmte Zeit ohne neue Änderungen vergangen ist. Alpine.js bietet keinen eingebauten Debounce-Mechanismus für $watch, aber die Implementierung ist unkompliziert mit einem Timer im Callback.
document.addEventListener('alpine:init', () => {
Alpine.data('searchWithDebounce', () => ({
query: '',
results: [],
loading: false,
_debounceTimer: null,
_throttleTimer: null,
_lastThrottleCall: 0,
init() {
// Debounce: API erst nach 350ms Pause aufrufen
this.$watch('query', (val) => {
clearTimeout(this._debounceTimer);
this._debounceTimer = setTimeout(() => {
if (val.trim().length >= 2) {
this.fetchResults(val);
} else {
this.results = [];
}
}, 350);
});
// Throttle: maximal einmal pro Sekunde eine Aktion ausführen
this.$watch('scrollPosition', (pos) => {
const now = Date.now();
if (now - this._lastThrottleCall >= 1000) {
this._lastThrottleCall = now;
this.updateStickyHeader(pos);
}
});
},
async fetchResults(query) {
this.loading = true;
try {
const res = await fetch(`/api/suggest?q=${encodeURIComponent(query)}`);
this.results = await res.json();
} finally {
this.loading = false;
}
},
destroy() {
// Timer aufräumen beim Destroy der Komponente
clearTimeout(this._debounceTimer);
clearTimeout(this._throttleTimer);
}
}));
});
5. Store-Watcher: Globale State-Änderungen beobachten
Mit der Punktnotation lassen sich auch tief verschachtelte Werte beobachten: $watch('$store.cart.count', ...). Das ermöglicht, in einer Komponente auf Änderungen eines globalen Stores zu reagieren, ohne den Store selbst zu modifizieren. Ein typisches Anwendungsbeispiel ist eine Warenkorb-Animation: Wenn die Warenkorb-Anzahl sich erhöht, soll das Header-Icon eine kurze Animation zeigen. Die Komponente beobachtet $store.cart.count und setzt ein Flag, das die Animation auslöst.
Store-Watcher folgen denselben Regeln wie Komponenten-Watcher: Sie müssen im init()-Hook registriert werden, lösen nicht beim Init aus, und reagieren bei tiefen Objektänderungen im Store ebenfalls, da Alpine.js Store-Objekte ebenfalls reaktiv macht.
6. x-effect vs. $watch: Wann was verwenden?
x-effect und $watch lösen ähnliche Probleme auf unterschiedliche Weise. x-effect beobachtet automatisch alle reaktiven Zugriffe innerhalb der Funktion und führt die Funktion neu aus, wenn einer davon sich ändert. $watch beobachtet explizit einen benannten Wert. x-effect wird sofort bei der Initialisierung ausgeführt und bei jeder Änderung erneut. $watch wird nicht bei der Initialisierung ausgeführt.
Die Faustregel: x-effect eignet sich für DOM-Seiteneffekte, die anfangs ausgeführt werden müssen und mehrere Werte beobachten. $watch eignet sich für Seiteneffekte, die nur bei echten Änderungen stattfinden sollen, die explizit an einen bestimmten Wert gebunden sind und bei denen der alte Wert bekannt sein muss.
7. Watcher aufräumen und Speicherlecks vermeiden
$watch gibt eine Cleanup-Funktion zurück, die den Watcher deregistriert. In Langlebigen Komponenten oder Einzelseitenanwendungen, wo Komponenten dynamisch hinzugefügt und entfernt werden, sollte diese Funktion im destroy()-Hook aufgerufen werden. Sonst bleiben Watcher aktiv, nachdem die Komponente aus dem DOM entfernt wurde, was zu Speicherlecks und unerwartetem Verhalten führen kann.
document.addEventListener('alpine:init', () => {
Alpine.data('managedWatcher', () => ({
value: '',
_watchers: [], // Sammlung aller Cleanup-Funktionen
init() {
// $watch gibt Cleanup-Funktion zurück
const stopWatcher1 = this.$watch('value', (newVal) => {
console.log('Value changed:', newVal);
// Seiteneffekte...
});
const stopWatcher2 = this.$watch('$store.cart.count', (count) => {
// Auf Store-Änderungen reagieren
this.animateCartIcon(count);
});
// Alle Cleanup-Funktionen sammeln
this._watchers.push(stopWatcher1, stopWatcher2);
// Event-Listener für externe Events
const handler = (e) => { this.value = e.detail; };
window.addEventListener('value-update', handler);
this._watchers.push(() => window.removeEventListener('value-update', handler));
},
animateCartIcon(newCount) {
if (newCount > 0) {
this.$dispatch('cart-updated', { count: newCount });
}
},
destroy() {
// Alle Watcher und Event-Listener aufräumen
this._watchers.forEach(stop => stop());
this._watchers = [];
}
}));
});
8. Praxispatterns: Suche, Filter, Formularvalidierung
Drei konkrete Anwendungsfälle zeigen, wie $watch in der Praxis eingesetzt wird. Bei der Suche mit Autocomplete beobachtet ein Watcher das Query-Feld und ruft nach Debounce die Autocomplete-API auf. Bei der URL-basierten Filterung beobachtet ein Watcher das gesamte Filter-Objekt und schreibt die aktive Filterauswahl in die URL – ohne Page-Reload, mit History-API. Bei der Formularvalidierung beobachtet ein Watcher jedes relevante Feld und führt Validierungsregeln aus, wenn sich der Wert ändert, mit unmittelbarem visuellen Feedback.
In Hyvä-Projekten ist das URL-Filter-Pattern besonders wertvoll für Produktlistenseiten. Der Watcher synchronisiert Filterauswahl und URL, sodass der Benutzer die aktuelle Filteransicht als Lesezeichen speichern oder teilen kann. Beim Zurücknavigieren mit dem Browser-Zurück-Button kann window.onpopstate die Filter aus der URL zurücklesen und den State wiederherstellen.
9. $watch im Vergleich: Alpine vs. Vue vs. React
Das Watcher-Konzept existiert in verschiedenen Frameworks mit unterschiedlicher API und Semantik. Ein direkter Vergleich verdeutlicht die Besonderheiten von Alpine.js $watch.
| Aspekt | Alpine $watch | Vue watch() | React useEffect |
|---|---|---|---|
| Auslösung beim Init | Nein (nur Änderungen) | Optional (immediate: true) | Ja (bei erstem Render) |
| Alter Wert | Ja (2. Argument) | Ja (2. Argument) | Via useRef manuell |
| Deep Watch | Automatisch für Objekte | Opt-in (deep: true) | Dependency Array manuell |
| Cleanup | Rückgabewert aufrufen | Rückgabewert aufrufen | Return-Funktion in useEffect |
| Build-Step nötig | Nein | Praktisch Pflicht | Pflicht (JSX) |
10. Zusammenfassung
$watch in Alpine.js ist das Werkzeug für explizite Seiteneffekte: Operationen, die außerhalb des DOM stattfinden und an eine konkrete State-Änderung gebunden sind. Die wichtigsten Charakteristika: Kein Auslösen beim Init, Zugriff auf alten und neuen Wert, Deep Reactivity für verschachtelte Objekte und eine Cleanup-Funktion als Rückgabewert. Im Vergleich zu x-effect ist $watch expliziter und vorhersagbarer, weil es genau einen Wert beobachtet statt alle Zugriffe innerhalb einer Funktion.
In der Praxis sind die häufigsten Anwendungsfälle: URL-Synchronisation mit der History API, Debounced-API-Aufrufe bei Sucheingaben, URL-basierte Filterung auf Produktlistenseiten, Store-Beobachtung für animierte Reaktionen auf globale State-Änderungen und Formularvalidierung mit unmittelbarem Feedback. In Hyvä-Projekten und Magento-2-Frontend-Entwicklung ist $watch oft die präzisere Alternative zu x-effect, wenn der Entwickler genau weiß, welchen Wert er beobachten möchte.
Alpine.js $watch — Das Wichtigste auf einen Blick
Registrierung
this.$watch('property', (newVal, oldVal) => {}) im init()-Hook. Löst nicht beim Init aus – nur bei echten Änderungen.
Cleanup
Rückgabewert von $watch ist eine Stop-Funktion. Im destroy()-Hook aufrufen, um Leaks zu vermeiden.
$watch vs. x-effect
$watch: explizit, kein Init-Aufruf, alter Wert verfügbar. x-effect: automatische Abhängigkeitserkennung, sofortiger Init-Aufruf.
Debounce
Kein eingebautes Debounce – mit setTimeout/clearTimeout im Callback implementieren. Timer im destroy() aufräumen.
Mironsoft
Alpine.js, Hyvä-Themes und Magento-2-Frontend-Entwicklung
Reaktive Alpine.js-Komponenten für euer Projekt?
Wir bauen reaktive Frontend-Komponenten mit Alpine.js – von URL-basierten Filtern über Autocomplete-Suche bis hin zu komplexen Formularvalidierungen für Hyvä und Magento 2.
Filterkomponenten
URL-synchronisierte Filter für Magento-2-Produktlisten mit $watch und History API
Suche & Autocomplete
Debounced-Suche mit Alpine.js-Watchern ohne jQuery oder externe Bibliotheken
Formularlogik
Reaktive Formularvalidierung mit $watch, unmittelbarem Feedback und Fehlertoasts
11. FAQ: Alpine.js $watch
1Wird $watch beim Init ausgelöst?
init()-Hook manuell erfolgen.2$watch vs. x-effect?
3Deep Watch für verschachtelte Objekte?
4Debounce mit $watch implementieren?
clearTimeout(this._timer); this._timer = setTimeout(() => {...}, 350); im Callback. Handle als Komponenten-Property speichern.5Store-Wert mit $watch beobachten?
this.$watch('$store.cart.count', callback) – funktioniert wie lokaler State-Watcher.6Was gibt $watch zurück?
destroy()-Hook aufrufen, um Leaks zu vermeiden.7Mehrere Watcher für denselben Wert?
8Array-Element beobachten?
this.$watch('items.0.name', ...). Für dynamische Indices besser x-effect nutzen.9Async-Callback möglich?
try/catch im async-Callback ist Pflicht.