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>
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';
}
},
},
};
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: {
},
});
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>
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');
}
},
},
});
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>
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>
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>
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>
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
- 23/03/2020: Added
unwatch
/unsubscribe
calls tobeforeDestroy
. Special thanks to @opolancoh for mentioning it in the comments.
Oldest comments (57)
Why you use Vue.set(state, 'status', status) instead of just state.status = status?
In this case it is not necessary and I could have use
state.status = status
, no worries.But as I'm more concerned with (vuejs.org/v2/guide/reactivity.html) I've got used to always write
Vue.set
to avoid any pitfalls while developing. So it is just a way to keep the codebase concise (in one mutation it isstate.prop = prop
and in the other isVue.set...
).Thank you a lot for your post. It's very useful !
Nice you liked it!
If you don't mind, could you describe where you've used (or plan to use) this strategy?
Of course. I'm writing a tool for designing formal models, and I needed a way to display warning messages from my components (e.g, after login/registration etc.).
Cool! Got your idea!
Thanks for sharing :D
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.
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?
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.
Super cool man! I think it is very unique use case this one!
Hmm, the usage of 'mounted' hook to fetch API seems to be contrast from what I have read recently.
Are you sure that is the correct place to put API call?
Hey!
Sorry for the late reply, I was on vacation for a week and attending a conference this week!
Yeah, I definitely agree that you might use it in the created hook, I use it like that most of the time. Nevertheless regarding the examples shown above it won’t have an impact in the logic itself. The big difference is if you want to access the DOM or not, or if you have SSR in place as well which can cause some inconsistencies as created hook runs in the server and mounted doesn’t.
Do you think it would add any value updating the examples?
Thanks!
From my previous read, I would love to see we can provide consistent tutorial and samples.
I also use
mounted
hook to fetch API due to non-SSR requirements. But then I'm working on some Nuxt projects so I need to change my own practice.If you can update the examples then it'll be perfect. Thank you.
It took a bit longer than what I expected but just updated the repository with the examples. I'm going to update the examples here as well. Thank you again for pointing it out!
It should be all up-to-date now. I've also checked the text itself to update any references to
mounted
.I got vuex store undefined when my application redirected from PayPal sandbox. The store got undefined and the auth middleware doesn't apply as expected.
Don't know how to fix this?
Hey Abu,
Could you give some extra input here? I didn't fully get what you mean. Could you maybe share a gist where I can try to understand your situation a bit better?
Thanks!
Thanks!
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 👌
Great! Really happy to "hear" 😅 that!
+1 for the subscribe method! Thx!
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.
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 :)Very helpful, been looking for this for some time now. Thank you very much
Thanks! Could you share your use case?
Or you could set getter as a function call, example:
isSearch: (state: CustomsState) => () => {
return state.isSearch
},
Hey @isabolic99 , sorry but I didn't get your point here. Could you add more context?
Thanks!
I have t say, I keep comming back to this article, great job writing this
Thank you very much!
I really appreciate the feedback!
If you have any tips on how to improve it (more examples, use cases, code rewriting...) I would love to hear!
Do I need to unsubscribe the mutation in the destroyed hook?
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 =[
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.
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 =]
Thanks!!
Hello,
I'm really grateful for this article
But using "computed: mapState(['status'])", denied you to put another computed func,
How did you solve this problem ?
Hey Cheddadi,
If I understood you correctly you want to have this
mapState
+ another computed property, right?If so, you can do it like that:
You can read more about it here: vuex.vuejs.org/guide/state.html#ob...
Hope it helps! Have a nice day!
Hey Neves,
Yes, its work fine now.
I'm just wondering about these three dot (...) !! What they do?
Nice to hear that!
It is called
Destructuring assignment
. You can read more about it here: developer.mozilla.org/en-US/docs/W...In short (from MDN)