In my Vue component I want to fetch work orders via axios. To do that, I need a user id, which is loaded via a computed property.
Just calling my method 'loadWorkorders' in the mounted lifecycle hook, results in the computed property not being available yet. So I want to check if this computed property is available, if not, just wait a few milliseconds and try again. Repeat the same cycle until it is available.
What is the best approach to handle this? Now I'm just waiting for a second and this seems to work, but I'm looking for a more robust approach.
My code:
export default {
name: "MyWorkorders",
mounted() {
if (this.user) {
this.loadWorkorders();
} else {
setTimeout(this.loadWorkorders(), 1000);
}
},
data() {
return {
workOrders: null,
};
},
methods: {
loadWorkorders() {
axios
.get(`/workorders/load-user-workorders/${this.user.id}`)
.then((res) => {
this.workOrders = res.data.workorders;
})
.catch((err) => {
console.log(err);
});
},
},
computed: {
user() {
return this.$store.getters.getUser;
},
},
};
If you are using Vue 3, consider using Async Components, which are also related to the Suspense built-in component. (at the time of this post, Suspense is an experimental feature, however Async Components are stable)
If you are using Vue 2, take a look at this part of the documentation about handling loading state in Async Components.
If you do not want to invest in these solutions because your use case is very, very simple -- you can try the following simple solution:
<template>
<template v-if="loading">
loading...
</template>
<template v-else-if="loaded">
<!-- your content here -->
</template>
<template v-else-if="error">
error
</template>
</template>
I have made this runnable example for demonstration. Of course, the templates for loading and error states can be made more complex if required.
Related
How can I detect changes of the getter value inside the template?
I have the following component:
computed: {
...mapGetters({
processingStep: 'products/processingStep',
}),
<div class="col" v-if="processingStep !=='last'">
...
...
</div>
So when I click the button in the Vuex state value for processingStep is getting changed during the time. The thing is that inside Vue dev tools I see updated getter value but my componnet template does not track it. How can this be fixed and how can above div be aware about processingStep value change?
You should be able to subscribe to your store's mutation. Subscribing is essentially like creating an event listener for any time a mutation is called on your store. For example, if your processingStep value is changed by a mutation called setProcessingStep:
export default {
data() {
return {
processingStep: null
}
},
mounted() {
this.$store.subscribe((mutation, state) => {
if (mutation.type === 'setProcessingStep') {
this.processingStep = state.processingStep
}
})
}
}
This is a basic example on how this should work. I tried to mock your setup.
Below is a working example of a getter being reactive to change the vue v-if in the DOM.
Maybe this example helps you spot an error you might have made in your code.
I will not add the example using a watcher, due to this being the correct way to use vuex with getters. Using a watcher would be avoiding the real problem and be considered bad practice.
I also suspect you might have broken the vue reactivity in your app.
If you look at the vuex docs: Mutations Follow Vue's Reactivity Rules and Vue Change Detection Caveats
This essentially means that Vue cannot detect changes applied for objects and arrays done in a specific way.
For objects: Typically when you add foreign keys that was not there on initialization
For Arrays: When you directly set an item with the index or change the length
Vue.use(Vuex);
const store = new Vuex.Store({
modules: {
products: {
strict: true,
namespaced: true,
state: {
step: 'first'
},
getters: {
processingStep(state) {
return state.step;
}
},
mutations: {
CHANGE_STEP(state) {
state.step = 'last'
}
}
}
}
});
const demo = new Vue({
el: '#demo',
store: store,
computed: {
...Vuex.mapGetters({
processingStep: 'products/processingStep'
})
},
methods: {
changeStep() {
this.$store.commit('products/CHANGE_STEP')
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vuex/2.3.1/vuex.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.3/vue.js"></script>
<div id="demo">
<p>processingStep: {{ processingStep }}</p>
<button #click="changeStep">change Step</button>
<div class="col" v-if="processingStep !=='last'">
First step! :D
</div>
<p v-else-if="processingStep !== 'first'">
This is the last..
</p>
</div>
I'd like to do something like this in mounted() {}:
await fetchData1();
await fetchData2UsingData1();
doSomethingUsingData1And2();
So I wonder if this works:
async mounted() {
await fetchData1();
await fetchData2UsingData1();
doSomethingUsingData1And2();
},
In my environment it raises no errors, and seems to work well.
But in this issue, async/await in lifecycle hooks is not implemented.
https://github.com/vuejs/vue/issues/7209
I could not find further information, but is it available in fact?
It will work because the mounted hook gets called after the component was already mounted, in other words it won't wait for the promises to solve before rendering. The only thing is that you will have an "empty" component until the promises solve.
If what you need is the component to not be rendered until data is ready, you'll need a flag in your data that works along with a v-if to render the component when everything is ready:
// in your template
<div v-if="dataReady">
// your html code
</div>
// inside your script
data () {
return {
dataReady: false,
// other data
}
},
async mounted() {
await fetchData1();
await fetchData2UsingData1();
doSomethingUsingData1And2();
this.dataReady = true;
},
Edit: As stated in the documentation, this is an experimental feature and should not be used in production applications for now.
The correct way to do this in vue3 would be to make your setup() function async like this:
<script>
// MyComponent.vue
export default defineComponent({
/* ... */
async setup() {
await fetchData1();
await fetchData2UsingData1();
doSomethingUsingData1And2();
this.dataReady = true;
}
}
</script>
And then use a suspense component in the parent to add a fallback like this:
<template>
<Suspense>
<template #default>
<MyComponent />
</template>
<template #fallback>
Loading...
</template>
</Suspense>
</template>
So you would see the #fallback template while the component is loading, and then the component itself when it's ready.
Just use $nextTick to call async functions.
I wrote a simple template-substitution component in VueJS as a single-file component. It doesn't have many features: just one prop, and I also made a computed property to encapsulate some tricky transformations that are done to that prop before it can be used in the template. It looks something like the following:
<template>
...some-html-here...
<a :href="myHref">...</a>
...
</template>
<script>
export default {
name: 'MyComponent',
props: {
href: { type: String, required: true },
},
computed: {
myHref() {
let result = this.href;
// several lines of complicated logic making substitutions and stuff
// ...
return result;
}
}
};
</script>
Now I think this should really be a functional component, as it has no state, no data, no reactivity, and so lunking around a whole instance is wasteful.
I can make this functional just by adding the 'functional' attribute to my <template>. In a functional component, of course, there are no such things as computed properties or methods or whatever. So my question is: where can I put my several lines of complicated logic? I don't want to have to embed this directly into my template, especially as it is used in multiple places. So where can I put code to transform my input props and make them ready to use in my template?
Great question.I was trying to find the same answer and i ended up with the following which i don't know if it is a good way to do though.
The "html" part:
<template functional>
<div>
<button #click="props.methods.firstMethod">Console Something</button>
<button #click="props.methods.secondMethod">Alert Something</button>
</div>
</template>
The "js" part:
<script>
export default {
props: {
methods: {
type: Object,
default() {
return {
firstMethod() {
console.log('You clicked me')
},
secondMethod() {
alert('You clicked me')
}
}
}
}
}
}
</script>
See it in action here
Make sure to read about functional components at docs
NOTE: Be aware using this approach since functional components are stateless (no reactive data) and instanceless (no this context).
I'm having issues where a computed getter accesses the state before it is updated, thus rendering an old state. I've already tried a few things such as merging mutations with actions and changing state to many different values but the getter is still being called before the dispatch is finished.
Problem
State is accessed before async action (api call) is complete.
Code structure
Component A loads API data.
User clicks 1 of the data.
Component A dispatches clicked data (object) to component B.
Component B loads object received.
Note
The DOM renders fine. This is a CONSOLE ERROR. Vue is always watching for DOM changes and re-renders instantly. The console however picks up everything.
Goal
Prevent component B (which is only called AFTER component) from running its computed getter method before dispatch of component A is complete.
Store.js
import Vue from 'vue'
import Vuex from 'vuex'
import axios from 'axios';
Vue.use(Vuex);
export const store = new Vuex.Store({
state: {
searchResult: {},
selected: null,
},
getters: {
searchResult: state => {
return state.searchResult;
},
selected: state => {
return state.selected;
},
},
mutations:{
search: (state, payload) => {
state.searchResult = payload;
},
selected: (state, payload) => {
state.selected = payload;
},
},
actions: {
search: ({commit}) => {
axios.get('http://api.tvmaze.com/search/shows?q=batman')
.then(response => {
commit('search', response.data);
}, error => {
console.log(error);
});
},
selected: ({commit}, payload) => {
commit('selected', payload);
},
},
});
SearchResult.vue
<template>
<div>
//looped
<router-link to="ShowDetails" #click.native="selected(Object)">
<p>{{Object}}</p>
</router-link>
</div>
</template>
<script>
export default {
methods: {
selected(show){
this.$store.dispatch('selected', show);
},
},
}
</script>
ShowDetails.vue
<template>
<div>
<p>{{Object.name}}</p>
<p>{{Object.genres}}</p>
</div>
</template>
<script>
export default {
computed:{
show(){
return this.$store.getters.selected;
},
},
}
</script>
This image shows that the computed method "show" in file 'ShowDetails' runs before the state is updated (which happens BEFORE the "show" computed method. Then, once it is updated, you can see the 2nd console "TEST" which is now actually populated with an object, a few ms after the first console "TEST".
Question
Vuex is all about state watching and management so how can I prevent this console error?
Thanks in advance.
store.dispatch can handle Promise returned by the triggered action handler and it also returns Promise. See Composing Actions.
You can setup your selected action to return a promise like this:
selected: ({commit}, payload) => {
return new Promise((resolve, reject) => {
commit('selected', payload);
});
}
Then in your SearchResults.vue instead of using a router-link use a button and perform programmatic navigation in the success callback of your selected action's promise like this:
<template>
<div>
//looped
<button #click.native="selected(Object)">
<p>{{Object}}</p>
</button>
</div>
</template>
<script>
export default {
methods: {
selected(show){
this.$store.dispatch('selected', show)
.then(() => {
this.$router.push('ShowDetails');
});
},
},
}
</script>
You can try to use v-if to avoid rendering template if it is no search results
v-if="$store.getters.searchResult"
Initialize your states.
As with all other Vue' data it is always better to initialize it at the start point, even with empty '' or [] but VueJS (not sure if Angular or React act the same, but I suppose similar) will behave much better having ALL OF YOUR VARIABLES initialized.
You can define initial empty value of your states in your store instance.
You will find that helpful not only here, but e.g. with forms validation as most of plugins will work ok with initialized data, but will not work properly with non-initialized data.
Hope it helps.
When using Vue Router with routes like /foo/:val you have to add a watcher to react for parameter changes. That results in somewhat annoying duplicate code in all views that have parameters in the URL.
This could look like the following example:
export default {
// [...]
created() {
doSomething.call(this);
},
watch: {
'$route' () {
doSomething.call(this);
}
},
}
function doSomething() {
// e.g. request API, assign view properties, ...
}
Is there any other way to overcome that? Can the handlers for created and $route changes be combined? Can the reuse of the component be disabled so that the watcher would not be necessary at all? I am using Vue 2, but this might be interesting for Vue 1, too.
One possible answer that I just found thanks to a GitHub issue is the following.
It is possible to use the key attribute that is also used for v-for to let Vue track changes in the view. For that to work, you have to add the attribute to the router-view element:
<router-view :key="$route.fullPath"></router-view>
After you add this to the view, you do not need to watch the $route anymore. Instead, Vue.js will create a completely new instance of the component and also call the created callback.
However, this is an all-or-nothing solution. It seems to work well on the small application that I am currently developing. But it might have effects on performance in another application. If you really want to disable the reuse of the view for some routes only, you can have a look at setting the key's value based on the route. But I don't really like that approach.
I used this variant without :key prop on router-view component.
routes.js:
{
path: 'url/:levels(.*)',
name: ROUTES.ANY_LEVEL,
props: true,
component: (): PromiseVue => import('./View.vue'),
},
view.vue
<template>
<MyComponent :config="config" />
</template>
---*****----
<script>
data: () => ({ config: {} }),
methods: {
onConfigurationChanged(route) {
const { params } = route
if (params && params.levels) {
this.config = // some logic
} else {
this.config = null
}
},
},
beforeRouteUpdate(to, from, next) {
this.onConfigurationChanged(to)
next()
},
}
</script>
Inside the component, I use the config as a property. In my case, reactivity is preserved and the component is updated automatically from parameter changes inside the same URL.
Works on Vue 2
vue3 and script setup:
watch(route, () => { fetch()})
in import:
import { watch } from 'vue';
import { useRoute } from 'vue-router';
const route = useRoute()