Make Vue template wait for global object returned by AJAX call - vue.js

I'm trying to wait for certain strings in a sort of dictionary containing all the text for buttons, sections, labels etc.
I start out by sending a list of default strings to a controller that registers all the strings with my CMS in case those specific values do not already exist. After that I return a new object containing my "dictionaries", but with the correct values for the current language.
I run the call with an event listener that triggers a dispatch() on window.onload, and then add the data to a Vuex module state. I then add it to a computed prop.
computed: {
cartDictionary() {
return this.$store.state.dictionaries.myDictionaries['cart']
}
}
So now here's the problem: In my template i try to get the values from the cartDictionaryprop, which is an array.
<h2 class="checkout-section__header" v-html="cartDictionary['Cart.Heading']"></h2>
But when the component renders, the prop doesn't yet have a value since it's waiting for the AJAX call to finish. And so of course I get a cannot read property of undefined error.
Any ideas on how to work around this? I would like to have the dictionaries accessible through a global object instead of passing everything down through props since it's built using atomic design and it would be insanely tedious.
EDIT:
Adding more code for clarification.
My module:
const dictionaryModule = {
namespaced: true,
state: {
dictionaries: []
},
mutations: {
setDictionaries (state, payload) {
state.dictionaries = payload
}
},
actions: {
getDictionaries ({commit}) {
return new Promise((resolve, reject) => {
Dictionaries.init().then(response => {
commit('setDictionaries', response)
resolve(response)
})
})
}
}
}
My Store:
const store = new Vuex.Store({
modules: {
cart: cartModule,
search: searchModule,
checkout: checkoutModule,
filter: filterModule,
product: productModule,
dictionaries: dictionaryModule
}
})
window.addEventListener('load', function () {
store.dispatch('dictionaries/getDictionaries')
})

I think you can watch cartDictionary and set another data variable.
like this
<h2 class="checkout-section__header" v-html="cartHeading"></h2>
data () {
return {
cartHeading: ''
}
},
watch: {
'cartDictionary': function (after, before) {
if (after) {
this.cartHeading = after
}
}
}
Because this.$store.state.dictionaries.myDictionarie is undefined at the the begining, vuejs can't map myDictionarie['core']. That's why your code is not working.
You can do this also
state: {
dictionaries: {
myDictionaries: {}
}
}
and set the dictionaries key values during resolve.

I also would have liked to see some more of your code, but as i can't comment your questions (you need rep > 50), here it goes...
I have two general suggestions:
Did you setup your action correctly? Mutations are always synchronous while actions allow for asynchronous operations. So, if you http client returns a promise (axios does, for example), you should await the result in your action before calling the respective mutation. See this chapter in the official vuex-docs: https://vuex.vuejs.org/guide/actions.html
You shouldn't be using something like window.onload but use the hooks provided by Vue.js instead. Check this: https://v2.vuejs.org/v2/guide/instance.html#Lifecycle-Diagram
EDIT: As a third suggestion: Check, whether action and mutation are called properly. If they are handled in their own module, you have to register the module to the state.

Related

Retrieve Data from Vuex Store using Axios

I've been searching around for possible answers to my question but I couldn't find anymore suggestions.
The structure of my project is as follows. I have a PoolMainPage where I show some information of the activePool Object. Within the PoolMainPage there are options to direct to subpages (TeamSelector and PoolStandings), which also require information of the activePoolObject.
The activePool Object is fetched from an endpoint in the Vuex store. The code for that is the following:
const actions = {
getActivePool({ commit }) {
const config = {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
withCredentials: true
}
const activePoolId = localStorage.getItem('activePoolId')
if (activePoolId) {
return axios.get('/pools/pool-information?poolId=' + activePoolId, config)
.then((response) => {
commit('SET_ACTIVE_POOL', response.data)
return response
})
.catch((error) => {
return Promise.reject(error)
})
} else {
commit('SET_ACTIVE_POOL', null);
return;
}
}
}
The endpoint does it's job and returns the activePool object. In my PoolMainPage component (as well as in the subcomponents) I want to call this action and fetch the object, using:
created() {
if (!this.$store.getters.activePool) {
this.$store.dispatch("getActivePool");
}
},
I added the check to prevent that the endpoint is called everytime the page is refreshed and the activePool is already set. To actually load the activePool into the component, I created a computed property:
computed: {
activePool() {
return this.$store.getters.activePool;
},
},
This all works when the endpoint has returned its data, but before that I receive an error on another computed property, which is dependent on the activePool object:
maxToSpend() {
return this.activePool.inGameBudget;
},
Question 1: how do I make sure that maxToSpend does not compute until the activePool actually set? I can simply add an additional check if (this.activePool) {, but then I would have to do that for all of the computed properties.
Question 2: I don't know if this is possible, but how do make sure that I don't have to add the code to fetch the activePool from the endpoint and get it using the computed property within each of the components that I created: TeamSelector and PoolStandings?
All help/suggestions are appreciated! Let me know if anything is unclear or requires some additional information.
Thanks!
Jeroen
How do I make sure that maxToSpend does not compute until the activePool actually set?
Basically you cannot do that. The computed properties are compute right after component create. See Lifecycle Diagram, computed properties are compute at Init injections & reactivity state.
I can simply add an additional check if (this.activePool) {, but then I would have to do that for all of the computed properties.
You case use Getters:
state: {
...
},
getters: {
maxToSpend: state => {
if (!state.activePool) return
return state.activePool.inGameBudget
}
},
actions: {
...
}
Then you can use as:
computed: {
maxToSpend () {
return this.$store.getters.maxToSpend
}
}
I don't know if this is possible, but how do make sure that I don't have to add the code to fetch the activePool from the endpoint and get it using the computed property within each of the components that I created: TeamSelector and PoolStandings?
Basically no. But if both TeamSelector and PoolStandings have common a parent (might be PoolMainPage?) then you can call it only once from that parent.
In my opinion the way to explicitly dispatch mandatory action for every page its needed it's not a bad idea.
You can mount activePool getter to avoid errors when accessing nested properties. Like:
// in getters.js
const activePool = (state) => ({
...state.activePool,
maxToSpend: state.activePool && state.activePool.inGameBudget,
})
activePool.inGameBudget will always be there, regardless the Promise. So now you can access this.activePool.inGameBudget; in .vue file and you wont get the error.

Returning a getters in a computed create a loop

I am calling inside the computed an action from the store to run it and after I am returning a getter, this will create a loop.
The HTML
{{loadedProjects}}
The computed
computed: {
loadedProjects() {
this.$store.dispatch("getProjects");
return this.$store.getters.loadedProjects;
}
}
The store
import Vuex from "vuex";
import axios from "axios";
const createStore = () => {
return new Vuex.Store({
state: {
loadedProjects: []
},
mutations: {
setProjects(state, projects) {
state.loadedProjects = projects
}
},
actions: {
getProjects(vuexContext) {
console.log("hello1")
return axios.get("THE API URL")
.then(res => {
console.log("hello2")
vuexContext.commit("setProjects", res.data);
})
.catch(e => console.log(e));
}
},
getters: {
loadedProjects(state) {
return state.loadedProjects;
}
}
});
};
export default createStore;
I expect to call my action to populate my state and after to return my state to render my data.
What is the point of using the store action that makes an API call inside the computed property ... maybe you want to trigger loadedProjects change ? ....computed property is not asynchronous so either way the return line will be executed before the you get the response... you might try vue-async-computed plugin OR just use the call on the created hook like you have done which is the better way and you don't have to use a computed property you can just {{ $store.getters.loadedProjects }} on your template
Computed properties should not have side effects (e.g. calling a store action, changing data, and so on). Otherwise it can happen that the triggered side effect could lead to a re-rendering of the component and possible re-fetching of the computed property. Thus, an infinite loop
I changed the code like that:
created: function () {
this.$store.dispatch("getProjects")
},
computed: {
loadedProjects() {
return this.$store.getters.loadedProjects
}
}
It is working now but I would like to know but I have that problem working inside the computed and also I wonder if it's the best solution. Any help????

Vue.js, VueX - cannot render data from API in a component

I want to make an API call to the server to fetch the data and then display them in a component. I have a created() method which dispatches an action to my store, which, in turn, commits the mutation to udpate my store with the data I got from the server. I also have computed method where I simply call the getter which fetches the data from the store. The code looks like this:
state
state: {
data: {
rides: []
}
}
component.vue
created() {
this.$store.dispatch('fetchUserRides');
}
computed: {
...mapGetters([
'userRides'
]),
}
store.js
//actions
fetchUserRides({ commit }) {
axios.get('/api/stats/current_week')
.then(response => {
commit('fetchUserRides', response)
})
.catch(error => {
commit('serverResponsError')
})
//mutations...
fetchUserRides(state, payload){
let rides = payload.data
rides.forEach((item) => {
state.data.rides.push(item)
})
//getters
userRides: state => {
let rides = state.data.rides
rides.sort(( a, b) => {
return new Date(a.date) - new Date(b.date);
});
return rides
}
I receive over 40 objects in the response, I did check it by console.log(state.data.rides) and they are there in 100%.
My problem is that when I log off and log back in again it throws an error "TypeError: Cannot read property 'sort' of null". But if I hit Refresh they appear fine. The login action redirects me to the page where I render this component. This looks like the computed property first tries to fetch data by the getter from the array before it is actually populated in the store. How can I make sure I get the array of objects in my component?
You probably need to set an empty array ([]) as an initial value to state.data.rides instead of null.
Another option will be to check that rides is truthy in your getters.
Something like:
if (rides) {
rides.sort(( a, b) => {
return new Date(a.date) - new Date(b.date);
});
}
return []
I was able to resolve my problem and it turns out I made a mistake. I completely forgot I set state.data.rides = null instead of an empty array state.data.rides = null, which would explain why the array was empty. It was a legacy code I had :)

Computed property needs to wait for async data

export default {
data() {
return {
projects: []
}
},
mounted() {
axios.get('...')
.then(({ data } => this.projects = data)
},
computed: {
personalProjects() {
return this.projects.filter(...)
},
commercialProjects() {
return this.projects.filter(...)
}
}
}
The computed properties need to wait for projects to be fetched from the server before setting the data. What's the proper way to do this?
I tried this:
watch: {
projects() {
this.personalProjects = this.projects.filter(project => project.type === 'personal')
this.commercialProjects = this.projects.filter(project => project.type === 'commercial')
}
},
but I got an error message: Computed property "personalProjects" was assigned to but it has no setter.
Should I set personalProjects and commercialProjects in data() instead?
What you are currently doing is the correct approach. Computed properties are reactive, derived, properties. They will reactively update whenever projects is updated by the data request.
In essence, your component's logic starts off with no projects, [] and if anyone asks for personal or commercial projects they are given the correct result: there are none of either, [].
However, whenever the component is mounted, it starts the process of loading the actual projects and whenever it's done, the whole dependency graph of projects will be reactively updated meaning personalProjects will be provide the correct result.

vuex ajax call and several components

I'd like to know whats the best practice for an ajax call with vuex and multiple components sharing the data from this call.
Do I store a loading, success and error property for that call in the store so it populates all the way through to the components? I'm not sure whats the best way. I also don't want to call the ajax from all the components since that defeats the purpose of having it called once.
Right now I'm storing as I mentioned above:
new Vuex.Store({
state: {
persons: {
data: null,
loading: false,
error: null
}
}
});
I do this for EACH api call and then I store it there. Any better ways?
This article describes a few patterns for implementing ajax calls and updating vuex state. Personally I agree with the author that method 3 of moving your ajax calls to the vuex store as actions because, as the article points out, it decouples state and presentation logic. This is also helpful because you can return a promise from your action to know when the state can be mutated
Code example from the article:
store = new Vuex.Store({
state: {
message: ''
},
mutations: {
updateMessage(state, payload) {
state.message = payload
}
},
actions: {
refreshMessage(context) {
return new Promise((resolve) => {
this.$http.get('...').then((response) => {
context.commit('updateMessage', response.data.message);
resolve();
});
});
}
}
});
Vue.component('my-component', {
template: '<div>{{ message }}</div>',
methods: {
refreshMessage() {
this.$store.dispatch('refeshMessage').then(() => {
// do stuff
});
}
},
computed: {
message: { return this.$store.state.message; }
}
});
So as you can see here, ajax calls are encapsulated as vuex actions. In a component that relies on data from an ajax call, the component can perform the action (this.$store.dispatch('refreshMessage').then...) which will make the ajax call and update the data in the store. Since your component already has a reactive property that depends on data from the store, it will update automatically once the store has new data.