Skip to content

Fix $watch firing callback when primitive value unchanged#4732

Merged
calebporzio merged 1 commit intomainfrom
fix-watch
Feb 1, 2026
Merged

Fix $watch firing callback when primitive value unchanged#4732
calebporzio merged 1 commit intomainfrom
fix-watch

Conversation

@calebporzio
Copy link
Collaborator

Problem

When using $watch on a deeply nested primitive property (e.g., $watch('foo.bar.baz', callback)), the callback fires even when the actual value hasn't changed. This happens when you replace a parent object with a new one that contains the same nested value:

Alpine.data('example', () => ({
  foo: { bar: { baz: 'hello' } },

  init() {
    this.$watch('foo.bar.baz', (value) => {
      console.log('changed!') // Fires even when value is still 'hello'
    })
  },

  update() {
    this.foo = { bar: { baz: 'hello' } } // Same value, but callback fires
  }
}))

Investigation

Tested Vue 3's actual runtime behavior to establish the expected behavior:

Scenario Vue fires callback?
Watch primitive, replace parent with same value No
Watch primitive, replace parent with different value Yes
Watch object, replace with new object (same contents) Yes

Vue compares primitives by value and objects by reference.

Alpine's watch() function in reactivity.js was calling the callback unconditionally whenever the effect re-ran, without comparing old and new values.

Solution

Added value comparison before firing the callback:

  • Primitives: Compare with !==, skip callback if unchanged
  • Objects: Always fire (since deep watching via JSON.stringify may have detected nested mutations, and object reference comparison would correctly fire for new objects anyway)
if (typeof value === 'object' || value !== oldValue) {
    // fire callback
}

Also moved oldValue assignment outside the conditional to ensure it's always updated synchronously, preventing stale comparisons on subsequent effect runs.

@calebporzio calebporzio merged commit 045b7bb into main Feb 1, 2026
2 checks passed
@calebporzio calebporzio deleted the fix-watch branch February 1, 2026 23:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant