I'm learning Vue.js coming from React. I'm confused about the updated life cycle hook. no matter what state I change I can't get it to run. I have a console.log inside the updated hook and I've set a setTimeout in the mounted hook. the setTimeout callback runs and changes the state which changes what is displayed on the DOM although I don't ever see the console.log from the updated hook. I'm not sure why the update is not firing
NOTE: I'm using Vue.js 3.
<template>
...other components
</template>
<script>
export default {
name: 'App',
data: () => ({
state: 'initial'
}),
updated () {
console.log('UPDATED!!!')
},
mounted () {
setInterval(() => {
this.state = 'changed'
console.log('changing')
}, 1000)
}
}
</script>
The updated lifecycle hook only runs when the template has been updated (was re-rendered), which would only occur when a property used in the template has changed values.
Since state was modified, and the updated hook did not run, that would imply that the template is not using state. Also note repeatedly setting state to the same value would only trigger the hook once for the new value (assuming the template needed state).
For example, the following snippet would trigger the updated hook periodically, as the timer callback toggles state, which is rendered in the template:
<template>
<div>{{ state }}</div>
</template>
<script>
export default {
name: 'App',
data: () => ({
state: 'initial'
}),
updated () {
console.log('UPDATED!!!')
},
unmounted () {
clearInterval(this._timerId)
},
mounted () {
this._timerId = setInterval(() => {
this.state = this.state === 'changed' ? 'initial' : 'changed'
console.log('changing')
}, 1000)
}
}
</script>
demo
That said, there's usually a better way to do things in Vue instead of the updated hook (e.g., using a watch on a specific prop), but that depends on your actual use case, which isn't clear in the question.
Related
I'm trying to follow the guide here to test an emitted event.
Given the following Vue SFC:
<script setup>
</script>
<template>
<button data-testid="credits" #click="$emit('onCredits')">Click</button>
</template>
and the following Cypress test:
import { createTestingPinia } from '#pinia/testing';
import Button from './Button.vue';
describe('<Button />', () => {
it('renders', () => {
const pinia = createTestingPinia({
createSpy: cy.spy(),
});
cy.mount(Button, {
props: {
onCredits: cy.spy().as('onCreditsSpy'),
},
global: {
plugins: [pinia],
},
});
cy.get('[data-testid=credits]').click();
cy.get('#onCreditsSpy').should('have.been.called');
});
});
My test is failing with
expected onCreditsSpy to have been called at least once, but it was never called
It feels weird passing in the spy as a prop, have I misunderstood something?
I solved such a situation with the last example within Using Vue Test Utils.
In my case the PagerElement component uses the properties 'pages' for the total of pages to render and 'page' for the current page additional to the 'handleClick'-Event emitted once a page has been clicked:
cy.mount(PagerElement, {
props: {
pages: 5,
page: 0
}
}).get('#vue')
Within the test I click on the third link that then emmits the Event:
cy.get('.pages router-link:nth-of-type(3)').click()
cy.get('#vue').should(wrapper => {
expect(wrapper.emitted('handleClick')).to.have.length
expect(wrapper.emitted('handleClick')[0][0]).to.equal('3')
})
First expectation was for handleClick to be emitted at all, the second one then checks the Parameters emitted (In my case the Page of the element clicked)
In order to have the Wrapper-element returned a custom mount-command has to be added instead of the default in your component.ts/component.js:
Cypress.Commands.add('mount', (...args) => {
return mount(...args).then(({ wrapper }) => {
return cy.wrap(wrapper).as('vue')
})
})
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.
I'm using vue CLI and I created multiple components
app.vue
import home_tpl from './home.vue';
new vue({
el : '#app',
components : { home_tpl },
created(){
this.$store.subscribe((mutation) => {
switch(mutation.type){
case 'listing':
alert();
break;
});
}
})
and then I have also a listener to home.vue
home.vue
export default{
created(){
this.$store.subscribe((mutation) => {
switch(mutation.type){
case 'listing':
alert();
break;
});
}
}
The problem is when I do this.$store.commit('listing',1); this this.$store.subscribe((mutation) => { trigger twice which is the expected behavior since I listen to the event twice from different file, is there a way to make it trigger once only per component?
The reason I call mutation listener to home.vue is that because there's an event that I want to run specifically to that component only.
You sample code listen to listing change for both app.vue and home.vue, but according to your posts, it seems they are interested in different kind of changes?
As commented, watch should be a better approach if you are only interested in a few changes rather than all the changes of the store. Something like:
// home.vue
new vue({
el : '#app',
components : { home_tpl },
created(){
this.$store.watch((state, getters) => state.stateHomeIsInterested, (newVal, oldVal) => {
alert()
})
}
})
// app.vue
export default{
created(){
this.$store.watch((state, getters) => state.stateAppIsInterested, (newVal, oldVal) => {
alert()
})
}
}
The difference is:
the subscribe callback will be called whenever there is a mutation in the store (in your case this may waste some unnecessary callback invocations). The callback method receives the mutation and the updated state as arguments
the watch will only react on the changes to the return value of the getter defined in its first argument, and the callback receives the new value and the old value as arguments. You can watch multiple states if required.
I am new to Vue. I am passing props to a child component and when the props change, I am using watch to catch the changes and assign them to this.query. I am able to log this change in watch and it is working. The prop value is changing on the page but mounted is not being called again, it is only being called once when the component is rendered for the first time,. I want to be able to do a fetch each time these props change.
I have tried to play around with updated and beforeUpdate but I am unable to log the props there and so don't know where I can fetch the data every time they change.
<template>
<div class="movie-list">
<h1>{{ query }}</h1>
//At the moment I am just rendering the query which is changing as the prop changes but I want to be able to render movies incrementally from a fetch in mounted() as this.query changes
</div>
</template>
<script>
export default {
props: ['query'],
name: 'SearchedMovies',
data: function () {
return {
movies: []
}
},
watch: {
query: function(newVal){
this.query = newVal
console.log('query in watch', this.query);
//this is logging each time it changes
}
},
updated () {
console.log('query in updated', this.query);
//this is not logging
},
beforeUpdate () {
console.log('query in beforeUpdate', this.query);
//this is not logging
},
mounted () {
console.log('this is the query in SearchedMovies', this.query);
//this is logging once when the component first renders
//I want to be able to do a fetch here each time this.query changes and save the results to this.movies.
}
}
</script>
The watch will be called any time the query prop changes. You are assigning the new value — which is already in the query prop — back to the query prop, which is a bad idea, because you should not modify props.
Just put your fetch inside the watch callback.
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.