x-data
Alpine
Alpine.js · $watch · Reaktivität · Seiteneffekte · Hyvä
Alpine.js $watch
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.

11 Min. Lesezeit $watch · x-effect · Deep Watch · Debounce · Store-Watcher Alpine.js 3.x · Hyvä · Magento 2

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?
Nein – nur bei echten Änderungen. Initialer Aufruf muss im init()-Hook manuell erfolgen.
2$watch vs. x-effect?
x-effect: automatische Abhängigkeiten, sofortiger Init-Aufruf. $watch: explizit, kein Init-Aufruf, alter Wert verfügbar.
3Deep Watch für verschachtelte Objekte?
Ja – Alpine.js macht Objekte reaktiv, $watch reagiert auf tiefe Änderungen. Alter und neuer Wert zeigen auf dasselbe Objekt bei Mutation.
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?
Eine Cleanup-Funktion, die den Watcher deregistriert. Im destroy()-Hook aufrufen, um Leaks zu vermeiden.
7Mehrere Watcher für denselben Wert?
Ja – separate Callbacks, alle werden ausgeführt. Jeder gibt eigene Cleanup-Funktion zurück.
8Array-Element beobachten?
Mit Punktnotation: this.$watch('items.0.name', ...). Für dynamische Indices besser x-effect nutzen.
9Async-Callback möglich?
Ja – Alpine wartet nicht auf das Promise. Fehlerbehandlung mit try/catch im async-Callback ist Pflicht.
10Wann x-effect statt $watch?
x-effect wenn Abhängigkeiten nicht bekannt, Init-Aufruf nötig, oder mehrere Werte ohne explizite Auflistung. $watch für klare explizite Bindung an einen Wert.