DEV Community

Cover image for Watch for Vuex State changes!
Vinicius Kiatkoski Neves
Vinicius Kiatkoski Neves

Posted on • Edited on • Originally published at dev.to

Watch for Vuex State changes!

This is my first post on Dev.to so I would appreciate any feedback which could help me to improve my overall writing and also things that I might have forgotten to write and explain about! First paragraph done so let's Vue!


Today someone asked a question on Slack about how to handle different status in a Vue component. What he wanted was something like that: You make a request and it has 3 basic status (pending/loading, success, failure/error). How to handle it in a Vue component? He asked a way to do it with Vuex (he was using Vuex) but I will take a step back as it is not necessary to use Vuex for it (but I will explore the Vuex world too).

First of all we have 3 status and we have to behave differently for each one of them. The snippet below shows a way of doing it:

<template>
  <h1 v-if="status === 'success'">Success</h1>
  <h1 v-else-if="status === 'error'">Error</h1>
  <h1 v-else>Loading</h1>
</template>
Enter fullscreen mode Exit fullscreen mode

It basically displays different messages based on the status which is the desired behavior.

Let's first assume that it is a single component and the requested data won't be needed anywhere else (parent or sibling components) which makes the approach simple (I will explore the others later on).

I will assume you're a bit familiar with Vue.js which means you know about created, methods and data. Now let's implement the desired behavior for that specific component (api.get is mocking an API request with 1s delay so we can see the transition in the status).

import api from '@/api';

export default {
  name: 'simple',
  data() {
    return {
      status: 'pending',
    };
  },
  created() {
    console.log(`CREATED called, status: ${this.status}`);

    this.handleCreated();
  },
  methods: {
    async handleCreated() {
      try {
        await api.get();

        this.status = 'success';
      } catch (e) {
        console.error(e);

        this.status = 'error';
      }
    },
  },
};
Enter fullscreen mode Exit fullscreen mode

There is no big deal here as everything is handled internally in the component which was not the case from the guy that asked this question. His context was a bit different I suppose. In his case the status needed to be shared among other components that weren't just children of this one. In this case we might have a shared state and that is where Vuex comes in (You can achieve the same with Event Bus and it is even better than just adding Vuex for this only state).

So now let's update our component to use the status from the Vuex Store instead of a local value. To do so first we create the status state.

export default new Vuex.Store({
  state: {
    status: 'pending',
  },
  mutations: {

  },
  actions: {

  },
});
Enter fullscreen mode Exit fullscreen mode

Now let's update our component to use the state.status:

<template>
  <h1 v-if="status === 'success'">Success</h1>
  <h1 v-else-if="status === 'error'">Error</h1>
  <h1 v-else>Loading</h1>
</template>

<script>
import { mapState } from 'vuex';

export default {
  name: 'vuex1',
  computed: mapState(['status']),
};
</script>
Enter fullscreen mode Exit fullscreen mode

Next step is to update the status after calling the API. We could achieve it the same way we were doing before, just referencing the status inside the Vuex Store but it is an extremely bad way of doing it. The right way of doing it now is dispatching a Vuex Action to handle it for us, so first we create the Action to handle it:

export default new Vuex.Store({
  state: {
    status: 'pending',
  },
  getters: {
    status: state => state.status,
  },
  mutations: {
    updateStatus(state, status) {
      Vue.set(state, 'status', status);
    },
  },
  actions: {
    async fetchApi({ commit }) {
      try {
        await api.get();

        commit('updateStatus', 'success');
      } catch (e) {
        console.error(e);

        commit('updateStatus', 'error');
      }
    },
  },
});
Enter fullscreen mode Exit fullscreen mode

It doesn't make sense to dispatch our Action from the component once we assumed that the State is shared among other components and we don't want each of them dispatching the same Action over and over. So we dispatch our Action in our App.vue file or any other component that makes sense for your application (Maybe in the main component of a view or so). Below is the snippet from the App.vue file dispatching the created Action:

<template>
  <div>
    <simple />
    <vuex1 />
  </div>
</template>

<script>
import Simple from '@/components/Simple.vue';
import Vuex1 from '@/components/Vuex1.vue';

export default {
  name: 'app',
  components: {
    Simple,
    Vuex1,
  },
  created() {
    this.$store.dispatch('fetchApi');
  },
};
</script>
Enter fullscreen mode Exit fullscreen mode

Cool, now it is working as expected! But I didn't tell you a thing. The problem he was trying to solve is a bit deeper than this one. He wants that some components that are being updated by this status behave differently when the status has changed. Imagine you might want to dispatch different actions for each component once this API calls succeed, how can you achieve that while you only dispatch the actions from the components that were rendered in the page?

My intention here is to show you few possibilities to handle this situation. One thing I agree in advance is that might sound an awkward situation for most of us but try to abstract the scenario I'm presenting to you and focus on what you can achieve from the features I'm showing here (You might have a completely different scenario where this solution fits a way better than here).

watch

Simplest way to achieve our desired solution. You can watch for a property update and handle it the way you want. In the example below we need to update a "complex" object otherwise our component will crash:

<template>
  <h1 v-if="status === 'success'">Success {{ complex.deep }}</h1>
  <h1 v-else-if="status === 'error'">Error</h1>
  <h1 v-else>Loading</h1>
</template>

<script>
import { mapState } from 'vuex';

export default {
  name: 'vuex2',
  data() {
    return {
      complex: null,
    };
  },
  computed: mapState(['status']),
  watch: {
    status(newValue, oldValue) {
      console.log(`Updating from ${oldValue} to ${newValue}`);

      // Do whatever makes sense now
      if (newValue === 'success') {
        this.complex = {
          deep: 'some deep object',
        };
      }
    },
  },
};
</script>
Enter fullscreen mode Exit fullscreen mode

Vuex watch

Did you know you can also use Vuex to watch for changes? Here are the docs. The only requirement is that it watches for a function that receives the State as the first param, the Getters as the second param and returns another function that will have its result watched.

There is one caveat once using Vuex watch: it returns an unwatch function that should be called in your beforeDestroy hook if you want to stop the watcher. If you don't call this function, the watcher will still be invoked which is not the desired behavior.

One thing to keep in mind here is that the reactivity takes place before the watch callback gets called which means our component will update before we set our complex object so we need to watch out here:

<template>
  <h1 v-if="status === 'success'">Success {{ complex && complex.deep }}</h1>
  <h1 v-else-if="status === 'error'">Error</h1>
  <h1 v-else>Loading</h1>
</template>

<script>
import { mapState } from 'vuex';

export default {
  name: 'vuex3',
  data() {
    return {
      complex: null,
    };
  },
  computed: mapState(['status']),
  created() {
    this.unwatch = this.$store.watch(
      (state, getters) => getters.status,
      (newValue, oldValue) => {
        console.log(`Updating from ${oldValue} to ${newValue}`);

        // Do whatever makes sense now
        if (newValue === 'success') {
          this.complex = {
            deep: 'some deep object',
          };
        }
      },
    );
  },
  beforeDestroy() {
    this.unwatch();
  },
};
</script>
Enter fullscreen mode Exit fullscreen mode

Vuex subscribe

You can subscribe for mutations which means your handler will be called whenever a mutation is commited (You can do the same for actions with subscribeAction). It is a bit trickier because we won't subscribe for a specific mutation only so we have to take care here.

There is one caveat once using Vuex subscribe: it returns an unsubscribe function that should be called in your beforeDestroy hook if you want to stop the subscriber. If you don't call this function, the subscriber will still be invoked which is not the desired behavior.

The drawback here is that we've lost the old value but as the first case it is called before the reactivity takes place so we avoid a double check if it is a concern. The result is shown in the snippet below:

<template>
  <h1 v-if="status === 'success'">Success {{ complex.deep }}</h1>
  <h1 v-else-if="status === 'error'">Error</h1>
  <h1 v-else>Loading</h1>
</template>

<script>
import { mapState } from 'vuex';

export default {
  name: 'vuex4',
  data() {
    return {
      complex: null,
    };
  },
  computed: mapState(['status']),
  created() {
    this.unsubscribe = this.$store.subscribe((mutation, state) => {
      if (mutation.type === 'updateStatus') {
        console.log(`Updating to ${state.status}`);

        // Do whatever makes sense now
        if (state.status === 'success') {
          this.complex = {
            deep: 'some deep object',
          };
        }
      }
    });
  },
  beforeDestroy() {
    this.unsubscribe();
  },
};
</script>
Enter fullscreen mode Exit fullscreen mode

Conclusion

As I mentioned earlier my idea here is not simply solving the problem that the guy from Slack came up with. I wanted to share a broader view of the solutions available and how to use them.

You may have a different problem where these solutions may have a good fit but as I did in this post here: Keep it simple! I started with a really simple solution for a specific problem and you should too. Wait until the performance issues or refactoring comes before you address complex solutions.

You can also check it out on Github if you want: vue-listen-to-change-example

Updates

Top comments (56)

Collapse
 
adamb_11 profile image
Adam Beguelin

Great post, thanks for taking the time to write it.

I'm trying to watch a Vuex user object and then do a Vuefire query when it gets populated.

I tried your suggestion but the watcher never fires. If I add the immediate flag, it will fire, but then the object I get doesn't seem to be the user object that I'm watching. Weird, right? (Not sure how to tell what object I'm getting, but it doesn't have any of the user fields like id or display name...)

gist.github.com/adamb/6b52d7127b39...

I this example the watcher is called but the object doesn't seem to be the user, because there is no user.id.

Also, the Vuex watcher is never called, but when the beforeDestroy() is called I get an error:

[Vue warn]: Property or method "handles" is not defined on the instance but referenced during render. Make sure that this property is reactive, either in the data option, or for class-based components, by initializing the property.

I looked at the docs and I am declaring handles[] so I'm not sure why it's complaining.

Any suggestions on what to try here?

Collapse
 
viniciuskneves profile image
Vinicius Kiatkoski Neves

Hey Adam, I'm glad that you liked it!

Let me try to help you from your gist.

You've a computed property called user which comes from your store, right? You don't watch computed properties (vuejs.org/v2/guide/computed.html). The computed property will have a set/get methods that you can customize for our needs. So you know when it changes (set is called).

If you want to watch, you can still do watch the getters.user so whenever the user changes (I'm assuming that is what you want here) this watcher will be called. I've tried to copy from your example the code below and adapt it but please double check the calls.

this.unwatch = this.$store.watch(
  (state, getters) => getters.user,
  (newUser, oldUser) => {
    this.$bind('handles', firebase.firestore().collection('handles'))
  },
)

What is happening here is:

  • You watch getters.user so I assume you've some user state that gets committed and a getter that maps to this state
  • Whenever the getters.user changes (new entry/property - watch out for Vue.set in this case) the watcher is called (where you want to call firestore)

I hope it is what you want (as I don't know in details it gets a bit tricky to fully understand the example). Let me know if I helps or let me know if anything else is needed.

Collapse
 
adamb_11 profile image
Adam Beguelin

Yes, you're correct. user is in the store and I'm trying to get data from Firestore when user changes. But this.$store.watch never gets invoked. If I console.log it in mounted it's null. But when I console.log it in beforeUpdate it's set. Yet, this.$store.watch is never called.

If I use the normal watcher on user it gets invoked, but the values don't seem to be there. I can log them and they are undefined. Perhaps this is because user is a Vuex computed property. I tried deep: true but then the (non-store) watcher is invoked twice, neither time with the data I need.

Any idea why the watcher isn't being called even through user is obviously changing?

Thanks for your help!

Thread Thread
 
viniciuskneves profile image
Vinicius Kiatkoski Neves

Hey Adam, to help our discussion I've created this Codesandbox: codesandbox.io/s/agitated-haze-ojdun

Please check store.js and App.vue. I've tried to create a fetch logic (full user and just username). Please mind the comments and play with comment/uncomment the deep and immediate options.
Also mind the first warning here vuejs.org/v2/api/#vm-watch once you hit the Fetch new username button.

Collapse
 
allanjeremy profile image
Allan N Jeremy

This saved me a tonne! Using it to fetch a user's id from the db (which may not be present at start). The user id is then used to determine which chats to show.

Before this, I kept getting null exceptions. Now I can just check for whether a value exists on change and act accordingly.

Even better, 0 errors now.
Thanks a lot Vin 👌

Collapse
 
viniciuskneves profile image
Vinicius Kiatkoski Neves

Great! Really happy to "hear" 😅 that!

Collapse
 
daviprm profile image
Davi Mello • Edited

Amazing!! ty sm 🧡

Collapse
 
maprangsoft profile image
Maprangsoft

thank you very much from thailand.

Collapse
 
viniciuskneves profile image
Vinicius Kiatkoski Neves

You are welcome, from Brazil 😅

Collapse
 
fidoogle profile image
Fidel Guajardo

Very nicely done. I used your information to automatically populate a Bank Name input field whenever a user enters a valid transit routing number (TR) in another input field. The TR is sent to a validation service on the server, which returns a JSON of the bank found, or an error. I watch for a "valid" routing number and populate the Bank Name from the "name" found in the JSON. If the routing is "invalid" then I ignore it and do nothing to the Bank Name.

Collapse
 
viniciuskneves profile image
Vinicius Kiatkoski Neves

Hey Fidel, thank you very much!

Really nice use case, thanks for sharing!

Was it easy to setup or did you find any problems in the way? Did you also find something else I could add to this post as a resource maybe?

Collapse
 
rolandcsibrei profile image
Roland Csibrei

IMHO there is a very important thing missing. If you want to watch for a FIELD change in an object or an item change in an array, you need to add deep:true to the watch method and use Vue.set to modify the watched object/array/field.

Collapse
 
viniciuskneves profile image
Vinicius Kiatkoski Neves

Hey Roland, thanks for your feedback.

That is true. I will update the article and add it (with links to the original docs about this corner case).

Nevertheless I strongly suggest to use Vue.set whenever possible to avoid pitfalls :)

Collapse
 
arik14 profile image
Faisal Arisandi • Edited

Hello, may I ask you something.
In above example, you were assigning return function from this.$store.subscribe into this.unsubscribe, also return function from this.$store.watch into this.unwatch.

But you never declare unwatch and unsubscribe in the data property.
That would mean unwatch and unsubscribe is added directly into local "this" in this Vue component.
Is it good practice?

Or should I add something like this,

data() {
  return {
    unwatch: null,
    unsubscribe: null
  }
},
methods: {
    ...
}
created() {
    ...
}
etc.
Enter fullscreen mode Exit fullscreen mode
Collapse
 
viniciuskneves profile image
Vinicius Kiatkoski Neves

Hey!

The difference is basically that I'm not watching these values so I don't need to declare them in the data property. The downside, of my approach, is lack of visibility (you've no clue, without reading the code, that these values exist). It is definitely better now if you use the Composition API.

I hope it helps, let me know if you still have questions!

Collapse
 
typerory profile image
Rory

Thank you. I'm monitoring a total count of video cameras in a reactive 'smart' shopping cart. If that number is greater than 4 then vuex queries the db for the model number of our recommended 8-channel NVR. I'm placing that model number in the state.

Now with your solution, I can just watch the state and replace the NVR in the cart with whatever model number is in the state.

Thank you. Thank you. Thank you.

Collapse
 
viniciuskneves profile image
Vinicius Kiatkoski Neves

Super cool man! I think it is very unique use case this one!

Collapse
 
theallenc profile image
AC1556 • Edited

Easy to understand and cater the solution to match my specific situation. One note though: I'm using Vuex as a substitute for event emitters and since my state values might not always be changing I had to use the vuex subscribe as watchers only respond to changes.

On that, it seems that Vuex can be really useful when trying to extend the functionality of event emitters; is this a recommended practice?

For example, I'm creating a simple showcase of UI elements and I have a form. The elements that make up the form would essentially be firing off events when they are clicked/modified/etc. As far as I can tell, using event emitters, the only way to respond to these events is to use v-on:<event name> on all of the parent elements. Given this:

That would mean putting @:btnClick="<fxn>" on every element. That just seems inefficient. This use-case is not ideal, but given an application that has a lot of large forms responding to events on every parent and then propagating it up seems like it could be a mess.

TLDR:

If I have a generic button component that I want to fire off a function that is held in a specific vue page, would using the button to modify the application vuex state be a good practice?

Thanks!

Collapse
 
viniciuskneves profile image
Vinicius Kiatkoski Neves

Hey @allen1556 , first of all thanks for the kind feedback!

Well, I will try my best to understand your issue and help you.

I think you're concern about adding some logic to your button, am I right? From what I understood you want to keep it "pure", which means that is just emits an event and the parent handles it the way it wants but as you've just shown, you might have 10x this button and have to add the props (which are different for each button) + the same handler over and over, right?

If I got it right, adding the handler to your button component might not allow you to use in other places. What you can do is to have a wrapper that handles this event (the wrapper forwards everything down to the component). The @click would just dispatch your action for example. So, short answer, yes, it is fine to update your state like that. The wrapping component would have this "store" logic and would make sense just on this part of your application and your button would still work anywhere else you need.

Did I get your idea right?

If not, please, let me try it again 😅

Collapse
 
opolancoh profile image
Oscar Polanco

Do I need to unsubscribe the mutation in the destroyed hook?

Collapse
 
viniciuskneves profile image
Vinicius Kiatkoski Neves

Very good question Oscar.

Yes, you do in case your component gets destroyed and the watcher is not needed anymore. You can read about it here: vuex.vuejs.org/api/#subscribe

I will update the examples and the post to add this information. Thanks for asking!

I'm just curious, could you share your use case? I can imagine something like navigating to a new route where the watcher doesn't make sense anymore but can't think about a lot of other use cases =[

Collapse
 
opolancoh profile image
Oscar Polanco • Edited

Hi Vinicius,

It's just to try to undesrstand the Observable pattern. When you unsubscribe, you cancel Observable executions and release resources. If you don't do it, you'll probably avoid memory leaks. I don't know if Vuex does that for you.

Thread Thread
 
viniciuskneves profile image
Vinicius Kiatkoski Neves

Hey Oscar,

Exactly. Once you subscribe you have to unsubscribe unless your Subject has some kind of check to remove the Observer in case it doesn't exist anymore.
For Vuex you've to do it manually, which means you've to unsubscribe once your component is destroyed.
If you don't do it you might end up in trouble as you mentioned =]

I didn't get if you were asking or not but I'm just confirming hehehe

In case you've any other question, let me know =]

Thread Thread
 
opolancoh profile image
Oscar Polanco

Thanks!!