Vuex accessing state BEFORE async action is complete - vue.js

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.

Related

Cypress spy not being called when VueJS component emits event

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')
})
})

Vuex is resetting already set states

Have started to play around with Vuex and am a bit confused.
It triggers the action GET_RECRUITERS everytime I load the component company.vue thus also making an api-call.
For example if I open company.vue => navigate to the user/edit.vue with vue-router and them go back it will call the action/api again (The recruiters are saved in the store accordinly to Vue-dev-tools).
Please correct me if I'm wrong - It should not trigger the action/api and thus resetting the state if I go back to the page again, correct? Or have I missunderstood the intent of Vuex?
company.vue
<template>
<card>
<select>
<option v-for="recruiter in recruiters"
:value="recruiter.id">
{{ recruiter.name }}
</option>
</select>
</card>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
middleware: 'auth',
mounted() {
this.$store.dispatch("company/GET_RECRUITERS")
},
computed: mapGetters({
recruiters: 'company/recruiters'
}),
}
</script>
company.js
import axios from 'axios'
// state
export const state = {
recruiters: [],
}
// getters
export const getters = {
recruiters: state => {
return state.recruiters
}
}
// actions
export const actions = {
GET_RECRUITERS(context) {
axios.get("api/recruiters")
.then((response) => {
console.log('API Action GET_RECRUITERS')
context.commit("GET_RECRUITERS", response.data.data)
})
.catch(() => { console.log("Error........") })
}
}
// mutations
export const mutations = {
GET_RECRUITERS(state, data) {
return state.recruiters = data
}
}
Thanks!
That's expected behavior, because a page component is created/mounted again each time you route back to it unless you cache it. Here are a few design patterns for this:
Load the data in App.vue which only runs once.
Or, check that the data isn't already loaded before making the API call:
// Testing that your `recruiters` getter has no length before loading data
mounted() {
if(!this.recruiters.length) {
this.$store.dispatch("company/GET_RECRUITERS");
}
}
Or, cache the page component so it's not recreated each time you route away and back. Do this by using the <keep-alive> component to wrap the <router-view>:
<keep-alive>
<router-view :key="$route.fullPath"></router-view>
</keep-alive>

how to make the nuxt child component wait until asyncData call get finish

For a form we have 2 components parent(for calling asyncdata and pass data as props to child) & child(form). I can properly fetch the props in child if I navigate using a link. But If I try to refresh the child component page it throws error as no props is passed. Found the reason to be that the parents asyncdata is not completing before the child render to sent the data in props.
Parent Component
<template>
<div>
<p>EDIT</p>
<NewListingModal :is-edit="true" :form-props="this.form" />
</div>
</template>
<script>
import NewListingModal from '#/components/NewListingModal.vue'
export default {
components: { NewListingModal },
async asyncData({ params, store }) {
const listing = await store.$db().model('listings').find(params.listing) //vuexorm call
if (typeof listing !== 'undefined') {
const convertedListing = JSON.parse(JSON.stringify(listing))
return {
name: '',
scrollable: true,
form: {names: convertedListing.names}
}
}
},
}
</script>
child component(other form data is removed to keep it understandable)
<template>
<div v-for="name in this.form.names" :key="name">
<p>{{ name }} <a #click.prevent="deleteName(name)">Delete<a /></a></p>
</div>
</template>
<script>
import Listing from '#/models/listing'
export default {
name: 'ListingModal',
props: {isEdit: {type: Boolean, default: false}, formProps: {type: Object}},
data() {
return {
name: '',
scrollable: true,
form: {names: this.formProps.names}
}
},
methods: {
addName() {
this.form.names.push(this.name)
this.name = ''
},
deleteName(name) {
const names = this.form.names
names.splice(names.indexOf(name), 1)
}
}
}
</script>
How can I make the NewListingModal component rendering wait until the asyncData completes in parent?
In my case, I used asyncData in my parent nuxt component, which fetches the data via store dispatch action, then set it to some store state key, via mutation.
Then I used validate method in my child component. Since Nuxt validate can return promises, I checked the vuex store first for fetched data. If there is none, I refetch it and return the promise instead.
In Parent component.vue
export default {
async asyncData({ params, store }) {
// Api operation which may take sometime
const {data} = await store.dispatch('fetch-my-data')
store.commit('setData', data) //This should be within above dispatch, but here for relevance
}
}
Here I am only fetching and saving to vuex store.
Child component.vue
export default {
async validate({ params, store }) {
let somedata = store.state.data //This is what you've set via parent's component mutation
return !!somedata || store.dispatch('fetch-my-data')
}
}
Here I am returning either the vuex store data (if exists), else refetch it.

Vue - Passing component data to view

Hi I'm looking at Vue and building a website with a Facebook login. I have a Facebook login component, which works, although I'm having difficulty making my acquired fbid, fbname, whatever available to my Vues outside the component. Acknowledged this most likely this is a 101 issue, help would be appreciated.
I've tried the global "prototype" variable and didn't manage to get that working. How's this done?
Code below:
main.js
new Vue({
router,
render: h => h(App),
vuetify: new Vuetify()
}).$mount('#app')
App.vue
...
import FacebookComp from './components/FacebookComp.vue'
export default {
name: 'app',
components: {
FacebookComp
},
...
}
Component - FacebookComp.vue
<template>
<div class="facebookcomp">
<facebook-login class="button"
appId="###"
#login="checkLoginState"
#logout="onLogout"
#get-initial-status="checkLoginState">
</facebook-login>
</div>
</template>
<script>
//import facebookLogin from 'facebook-login-vuejs';
//import FB from 'fb';
export default {
name: 'facebookcomp',
data: {
fbuserid: "string"
},
methods: {
checkLoginState() {
FB.getLoginStatus(function(response) {
fbLoginState=response.status;
});
if(fbLoginState === 'connected' && !fbToken){
fbToken = FB.getAuthResponse()['accessToken'];
FB.api('/me', 'get', { access_token: fbToken, fields: 'id,name' }, function(response) {
fbuserid=response.id;
});
}
},
...
}
VIEW - view.vue
...
export default {
name: 'view',
data: function() {
return {
someData: '',
},
mounted() {
alert(this.fbloginId); //my facebook ID here
},
...
}
If you would like to have all these props from FB login available throughout your whole app, I strongly suggest using vuex. If you are not familiar with state management in nowadays SPAs, you basically have global container - state. You can change it only via functions called mutations (synchronous). However, you cannot call them directly from your component. That's what functions called actions (asynchronous) are for. The whole flow is: actions => mutations => state. Once you have saved your desired data in state, you can access it in any of your vue component via functions called getters.
Another option, in case you just need the data visible in your parent only, is to simply emit the data from FacebookComp.vue and listen for event in View.vue. Note that to make this work, FacebookComp.vue has to be a child of View.vue. For more info about implementation of second approach, please look at docs.

Vuex mutation subscription trigger multiple times

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.