Watchers

Watchers execute side effects when reactive properties change. Use them for async operations, logging, or triggering actions that don't belong in computed properties.

The Basics

Define watchers in the watch object. Each watcher receives the new value and old value when its target property changes:

<div id="app">
    <input data-pac-bind="value: searchQuery" placeholder="Search...">
    <p>{{message}}</p>
</div>

<script>
    wakaPAC('#app', {
        searchQuery: '',
        message: '',

        watch: {
            searchQuery(newValue, oldValue) {
                if (newValue.length > 2) {
                    this.message = `Searching for: ${newValue}`;
                    console.log(`Changed from "${oldValue}" to "${newValue}"`);
                } else {
                    this.message = '';
                }
            }
        }
    });
</script>

Watchers execute after the property has been updated and the DOM has been queued for update. They're designed for side effects, not data transformations.

Async Operations

Watchers can be async functions, making them ideal for API calls and asynchronous operations:

<div id="app">
    <input data-pac-bind="value: userId" placeholder="Enter user ID">
    <p data-pac-bind="visible: loading">Loading...</p>
    <div data-pac-bind="if: user">
        <h3>{{user.name}}</h3>
        <p>{{user.email}}</p>
    </div>
    <p data-pac-bind="visible: error">{{error}}</p>
</div>

<script>
    wakaPAC('#app', {
        userId: '',
        user: null,
        loading: false,
        error: '',

        watch: {
            async userId(newId, oldId) {
                if (!newId) {
                    this.user = null;
                    return;
                }

                this.loading = true;
                this.error = '';

                try {
                    const response = await fetch(`/api/users/${newId}`);
                    this.user = await response.json();
                } catch (err) {
                    this.error = 'Failed to load user';
                    this.user = null;
                } finally {
                    this.loading = false;
                }
            }
        }
    });
</script>

Watching Objects with Nested Properties

When you watch an object, the watcher fires whenever any nested property changes:

wakaPAC('#app', {
    settings: {
        theme: 'light',
        fontSize: 16
    },

    watch: {
        settings(newSettings, oldSettings) {
            // When this.settings.theme = 'dark' is called:
            // newSettings = { theme: 'dark', fontSize: 16 }
            // oldSettings = { theme: 'light', fontSize: 16 }
            console.log('Theme changed from', oldSettings.theme, 'to', newSettings.theme);
        }
    }
});
How it works: Changing this.settings.theme = 'dark' triggers the settings watcher with complete before/after objects.

Watching Arrays

Watchers do not trigger for arrays or array element changes.

Important Caveats

Avoid Infinite Loops

Never modify the watched property inside its own watcher, as this creates an infinite loop:

// ❌ WRONG - Infinite loop
watch: {
    count(newValue) {
        this.count = newValue + 1;  // Triggers watcher again!
    }
}

// ✅ CORRECT - Modify different properties
watch: {
    count(newValue) {
        this.doubleCount = newValue * 2;  // Safe
    }
}

Watchers Don't Run on Initialization

Watchers only execute when properties change, not during initial component creation. If you need initialization logic, add it to your component's setup or use a lifecycle hook.

Race Conditions with Async

When using async watchers, later calls may complete before earlier ones. Handle this appropriately:

watch: {
    async userId(newId) {
        const requestId = ++this._requestId || 1;
        const response = await fetch(`/api/users/${newId}`);

        // Only update if this is still the latest request
        if (requestId === this._requestId) {
            this.user = await response.json();
        }
    }
}

Best Practices

  • Keep watchers focused: Each watcher should handle one specific concern
  • Prefer computed for derivations: If you're transforming data for display, use computed properties
  • Handle errors in async watchers: Always use try/catch for async operations
  • Clean up side effects: Clear timeouts, abort fetch requests, or remove event listeners when appropriate
  • Don't overuse watchers: Too many watchers can make data flow hard to trace