Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ability to disable/not trigger watch handler on data? #1829

Closed
dalanmiller opened this issue Nov 19, 2015 · 28 comments
Closed

Ability to disable/not trigger watch handler on data? #1829

dalanmiller opened this issue Nov 19, 2015 · 28 comments

Comments

@dalanmiller
Copy link

For my application, I'm mutating the object in my data: [{...}, {...}, {...}] to state changes occurring at other open instances of my application (happening through web sockets etc, etc). I have a handler on the data structure like this below:

watch: {
    todos: {
        deep: true,
        handler: todoStorage.save,
    },
},

Triggering todoStorage.save would unnecessarily save the contents of the array back to my database where I already know the current state of the application.

Is there a way to mutate the array without triggering the handler? It seems that trying to undefine the handler while making the operation doesn't work.

@dalanmiller dalanmiller changed the title Ability to disable watch handler on data? Ability to disable/not trigger watch handler on data? Nov 19, 2015
@yyx990803
Copy link
Member

The point of a watcher is that it will fire when the data changes. Instead of thinking about stopping it from firing, just do a conditional check inside the watcher callback.

@dalanmiller
Copy link
Author

I wish I could @yyx990803 😢

In this case I cannot do a conditional check in the watcher callback since there's no way for me to check whether I'm going to receive more deletion messages via WebSocket which come one at a time. If every object in my data Array is saved after every modification to the Array I get into this case:

  1. I receive deletion message for an object in data
  2. Save all remaining documents in data to DB
  3. Receive another deletion message for a different object in data which was just saved in previous step.
  4. Possible further repetition depending on number of deleted objects.

This results in errors on my backend because I'm trying to update data that no longer exists there. Is there a recommended Vue.js pattern for this?

@yyx990803
Copy link
Member

I don't think I understand your use case, but maybe using a deep watcher to persist the whole is just the wrong idea to begin with. You should probably use a component for each item in the array so you can do fine-grained persistence.

@matyo91
Copy link

matyo91 commented Nov 16, 2016

Hello, i guess i understand the issue from @dalanmiller.

In fact, i was stuck on a similar issue : the fact that you don't want the watcher trigger the callback every time when you manually decide to affect the value somewhere.

For instance : i got my data from an rest api call in an async way. Then i decide to affect the data in my component, but the watcher detect change and will trigger the callback that notice update the changes to my rest api : there were no data change but i got an api 'Set' call ! 😕

More into this issue, i did a codepen to explain that (VueJS 2.0) : http://codepen.io/matyo91/pen/ZBpjVz
📝 What i want is to fire change only when i manually change the value into the textarea.
Here, the 'code' variable will call onUpdateCode on sync change every time => not what i want 🐛 . And the 'coder' variable will not call onUpdateCoder because i did add a dirty silence mechanism. And onUpdateCoder is only called when i do change text into the coder textarea.
The coder is what i want 🍎 (i tried a better solution with Vue.nextTick, but it fail, i think it could have worked if Vue.nextTick can dispatch by setting a priority parameter)

I come to this code, because somehow, i got a similar issue when integrating ace.js into a VueJS component. And the ace team got the same issue and it's resolved by that comment from @nightwing : ajaxorg/ace#503 (comment)
The next comment from @davidthornton notice it's a https://en.wikipedia.org/wiki/Operational_transformation logic.

@yyx990803, you did already answered this in this issue : #1157
But it's not really this case here.

So i think it's more a design pattern issue. But now i don't know how to write it into a "smart code".

Any idea ?!

@njleonzhang
Copy link

It seems I encounter a similar issue, my solution is that use this.$watch instead of watch option, this.$watch return a handler to unwatch function, which I can call it when I want to stop watch, after that I watch the value again, it's terrible........

@thuijzer
Copy link

thuijzer commented Dec 20, 2016

Had the same issue. But I think the fix is simple. Just use the watch function to decide if you would like to call a method:

// vue 1.0
this.$watch('myModel', function(newVal, oldVal) {
    if([your statement]) {
        this.loadSomeData();
    }
});

@intijk
Copy link

intijk commented Jan 31, 2018

I found that action of the firefox and chrome is different, in updated(), if the event was triggered, firefox fall into stuck and never come out, but chrome can deal with this correctly.

@mikob
Copy link

mikob commented Feb 5, 2019

whether you use $watch and unwatch, or set a flag to prevent the watch from handling, you'll need to wait for this.$nextTick() otherwise you will unwatch/turn off the flag prematurely and the watcher will still get executed.

@doncatnip
Copy link

doncatnip commented Mar 15, 2019

It should be pretty obvious at this point that this would be a rather useful feature in many situations.

Ugly:

resetForm() {
    this.workbench.isResetting = true
    value.tier = value.original_tier
    this.nextTick(()=>{
        this.workbench.isResetting = false
    })
}

watch: {
    'value.tier'(new_,old) {
        if (this.workbench.isResetting)
            return;
        //...
    }
}

Nice:

resetForm() {
    Vue.set(value,'tier',value.original_tier, false)
}

Nice >> Ugly

@danielfaust
Copy link

danielfaust commented Oct 15, 2019

I'd rather have some kind of source added to the watch. Something along the lines of

<input type="checkbox" id="checkbox" v-source="my-html" v-model="light_is_on">
<label for="checkbox">Kitchen light is: {{ light_is_on ? "on" : "off" }}</label>

watch: {
  light_is_on: function(value, old, source) {
    if (source !== this.websocketHandler) {
      websocket.send({'light_is_on': value})
    }
    if (source == 'my-html') {
      console.log('watch triggered from HTML element')
    }
  },
},
methods: {
  websocketHandler(event) {
    if (event.light_is_on !== undefined) {
      this.light_is_on = event.light_is_on;
    }
  },
},

@Gruski
Copy link

Gruski commented Nov 11, 2019

I like the idea of getting the source from the watch function(value, old, source). I have the same problem and there are only ugly solutions.

@irongomme
Copy link

Hello, here is what i'm doing for that problem. I had to face with reseting twice search and filters, without trigger multiple watch at the same time.

data: {
  search: null,
  filters: { filter1: null, filter2: null },
  watchInPause: false,
},
watch: {
  search() {
    if (!this.watchInPause) {
      this.refresh();
    },
  },
  filters() {
    handler() {
      if (!this.watchInPause) {
        this.refresh();
      }
    },
    deep: true,
  },
},
async onResetFilters() {
  this.watchInPause = true;
  this.search = null;
  this.filters = {
    filter1: null,
    filter2: null,
  };
  await this.refresh();
  this.watchInPause = false;
},

Hope this helps ...

@abellion
Copy link

abellion commented May 5, 2020

I made a mixin which brings a $withoutWatchers() method. This method disables the watchers within its callback :

$withoutWatchers(() => {
  // Watchers will not get fired
})

The following is a real world example of a user's form. We want this component to take the user as a prop, copy it in an internal value, dispatch the changes when this internal value is updated, and update the internal value when the prop changes :

export default {
  data: () => ({
    model: null
  }),

  props: {
    user: {
      type: Object,
      required: true
    }
  },

  created () {
    this.$watch('user', this.sync, {
      immediate: true
    })
  },

  methods: {
    sync () {
      this.$withoutWatchers(() => {
        this.model = { ...this.user }
      })
    }
  },

  watch: {
    model () {
      this.$emit('update:user', this.model)
    }
  }
}
Here is the mixin

const WithoutWatchers = {
  methods: {
    $withoutWatchers (cb) {
      const watchers = this._watchers.map((watcher) => ({ cb: watcher.cb, sync: watcher.sync }))

      for (let index in this._watchers) {
        this._watchers[index] = Object.assign(this._watchers[index], { cb: () => null, sync: true })
      }

      cb()

      for (let index in this._watchers) {
        this._watchers[index] = Object.assign(this._watchers[index], watchers[index])
      }
    }
  }
}

Be aware that this mixin uses the internal Vue's API, which may change between minor versions. That would be cool to have it within Vue's core. Could we consider bringing it @yyx990803 ?

@nftopham
Copy link

@abellion wonderful solution to this problem.

@johnantoni
Copy link

johnantoni commented Aug 16, 2020

Nice work @abellion would love to get this added to vue

@hhaensel
Copy link

@abellion really nice!

  • What about using active: false instead of cb: () => null
  • Can you comment on what is more performant?
this._watchers[index] = Object.assign(this._watchers[index], { active: false, sync: true })

or

ww[index].active = false;
ww[index].sync = true

@hhaensel
Copy link

@abellion really nice!

  • What about using active: false instead of cb: () => null
  • Can you comment on what is more performant?
this._watchers[index] = Object.assign(this._watchers[index], { active: false, sync: true })

or

ww[index].active = false;
ww[index].sync = true

I had tried to use active and it seemed to work - but later failed, maybe, something was not updated. So I went back to using @abellion 's original version.
I'd be happy to understand, what the difference is and why sync = true is needed.

@hutch78
Copy link

hutch78 commented Apr 22, 2021

@abellion THANK YOU! This is very helpful. Battling data properties and watchers has been a thorn in my side for a long time.

@haolian9
Copy link

i figured out another solution, in my small project, it works well...

{
    data() {
        return {
            avoid_changes: 0,
            count: 1,
        }
    },
    watch: {
        count: function(a, b) {
            if (this.avoid_changes > 0) {
                this.avoid_changes -= 1
            } else {
                // triggered
            }
        }
    }
}

vm.avoid_changes += 1
vm.count = 9

@Arturexe
Copy link

Arturexe commented Mar 14, 2022

I don't know if this helps anyone but I needed a watcher that watches state.title only for the first change. This worked for me:


const watchOnce = watch(() => state.title, () => {
    if (state.title) {
        console.log(state.title)
        watchOnce()
    }
})

Working with vue 3 script setup

@h-sigma
Copy link

h-sigma commented May 28, 2022

In my case, I only wanted to watch a partial object.

      pagination: {
        page: 1,
        perPage: 5,
        totalPages: 0,
        totalRecords: 0,
      },

totalPages and totalRecords are set from the API response, while page and perPage are set from the UI.
I was able to bypass the issue by making a computed property that only watches the UI-changeable parts of the paginator.

@timmaier
Copy link

timmaier commented Sep 1, 2022

There's a watchIgnorable in the vueuse library: https://vueuse.org/shared/watchignorable/ that allows you to stop the watch

@mralston
Copy link

Here's a follow up to @abellion's solution.

As abellion predicted, Vue's internal architecture seems to have changed between versions 2 and 3, and I found that the mixin no longer worked after upgrading.

After a little digging, I found that the watchers had moved within Vue's hierarchy, but the basic premise appears to be same.

Here's an updated version of @abellion's mixin. I believe this works correctly in Vue 3. However I strongly suggest that anyone deciding to adopt it should test it carefully.

export default {
    methods: {
        $withoutWatchers (cb) {
            const watchers = this._.type.watch;

            for (let index in this._.type.watch) {
                this._.type.watch[index] = Object.assign(this._.type.watch[index], { cb: () => null, sync: true })
            }

            cb()

            for (let index in this._.type.watch) {
                this._.type.watch[index] = Object.assign(this._.type.watch[index], watchers[index])
            }
        }
    }
}

@jsodeman
Copy link

For anyone finding this still using Vue 2, as of 2.7.x abellion's solution above needs to be changed to:

 export default {
	methods: {
		$withoutWatchers(cb) {
			const watcher = {
				cb: this._watcher.cb,
				sync: this._watcher.sync,
			};

			this._watcher = Object.assign(this._watcher, { cb: () => null, sync: true });

			cb();

			this._watcher = Object.assign(this._watcher, watcher);
		},
	},
};

@john-
Copy link

john- commented Oct 14, 2023

There's a watchIgnorable in the vueuse library: https://vueuse.org/shared/watchignorable/ that allows you to stop the watch

Thanks for pointing this out. I ran against the issue and was able to resolve it by using watchIgnorable in my Vue 3 application.

Keywords: Parent initiated update alters property in child. Child UI to ignore updates as a result of the property change. Code

@mralston
Copy link

Anyone got any ideas on how to do this in Vue 3 with the Composition API and the <script setup> tag?

@john-
Copy link

john- commented Dec 29, 2023

Anyone got any ideas on how to do this in Vue 3 with the Composition API and the <script setup> tag?

The code I linked to on 10/14 worked for me in this situation.

@mralston
Copy link

Anyone got any ideas on how to do this in Vue 3 with the Composition API and the <script setup> tag?

The code I linked to on 10/14 worked for me in this situation.

Thank you! I'll take a look. :)

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

No branches or pull requests