Async Vue lifecycles - vue.js

I have the following situation in my Vue.js application:
data() {
return {
data: []
}
},
async created() {
console.log('before async call')
try {
// async call ...
console.log('after async call')
this.data = // data from api call
} catch (err) {
// handle err
}
},
mounted() {
console.log('mounted')
init(this.data)
}
When I run the code I get:
before async call
mounted
after async call
As a result of this the init method which is a class constructor in mounted gets called with an empty array instead of the data from the API call. What I would like is execute things synchronously and not execute mounted until the data is available. I understand that the above problem is how Vue executes lifecycles when there is async code included, but how can you solve problems like these?

async lifeCycles in Vue are a misleading syntax.
Every Vue lifecycle is only a trigger to run whatever code you put in there at that particular time.
But Vue doesn't wait for the promise to resolve and hold everything else (related to the component's lifecycle) until it happens. In effect, all you do is delay the execution of the code you place in the lifecycle until some promise resolves.
To give you a better understanding of what's going on, the following syntaxes are equivalent:
async created() {
const data = await fetchSomeData();
// rest of code depending on `data`
}
The equivalent:
created() {
fetchSomeData().then(data => {
// rest of code depending on `data`
});
}
Because async lifecycles are a misleading syntax, it's typically discouraged in applications developed by large teams, in favor of the .then() syntax. This is to avoid minor bugs created by misunderstanding when code actually runs. For example, if a new developer places some code into an async hook (without looking closely at the rest of the code in the hook), the code might run later than intended, unless placed before any await.
To fix whatever errors you're trying to fix, just wrap what can't be rendered until actual data has resolved into a if (inside component) or v-if (inside template).
Typical usage examples:
computed: {
someComputed() {
if (this.data.length) {
// returned when data has length
return something
}
// returned when data has no length
return somethingElse
}
}
or:
<div v-if="data.length">
<!-- markup depending on actual data... -->
</div>
<div v-else>
loading...
</div>
Note: the computed above will automatically react to the change in data's length (no need for a watch), because computed properties get re-run whenever their internal reactive references change values. As you'd expect, they recalculate/rerender anything depending on them. Same is true for <v-if>. Internally they both use what's known as Vue's "injections and reactivity".

If I were you, I would put all the logic within either mounted or created hook. If for some reason you need to wait until mount for your code to work, fetch the data and init it within mounted:
async mounted() {
try {
// async call ...
console.log('after async call')
this.data = await // data from api call
init(this.data)
} catch (err) {
// handle err
}
}
You've almost nothing to gain in terms of performance to split the code between created and mounted.
Still, if for some reason you really need to place them in different hooks, you could store data in a promise:
async created() {
console.log('before async call')
try {
// async call ...
console.log('after async call')
this.data = fetch('whatever') // now this.data is a promise
} catch (err) {
// handle err
}
},
async mounted() {
console.log('mounted')
init(await this.data)
}
This should work, but I don't think it would be worth the hassle.
As a clarification for whoever may read this: you can use async/await in vue lifecycle hooks in order to code within to await for promises etc. However, this won't make Vue to actually await for the lifecycles itselves: so you can end withmounted code running before async code from created. The fact that Vue doesn't wait for async lifecycles doesn't mean is bad to place async/await code in them.

Related

Vue2 Composition Api - How do I fetch data from api?

I am using Vue2.6 with composition api.
I need to reroute to different pages depends on an api response.
Can someone please guide me, please?
I tried using onBeforeMount but it renders the UI elements then rerouted to the corresponding page to the api response..so I can see a flash of the wrong UI..
setup() {
const myData = 'myData';
onBeforeMount(async () => {
try {
const results = await fetchData();
// do reroute depends on results response
}
} catch (err) {
console.log(err);
}
});
return {
myData,
};
I also tried adding async in the setup method but it errored saying my ref variables "Property or method "myData" is not defined on the instance but referenced during render."
async setup() {
const myData = 'myData';
onMounted(async () => {
try {
const results = await fetchData();
// do reroute depends on results response
}
} catch (err) {
console.log(err);
}
});
return {
myData,
};
It looks like you're trying to handle routing (re-routing) dynamically from inside a component. I can't see the rest of the apps, so can't speak to the validity of such a solution, but would like you dissuade you from doing that. routing logic should, IMO, not be handled in a component. The components should mostly just handle the template and user interaction. By the time you're rendering a component, that API should have been resolved already.
I would recommend to resolve the API response before the route is
even completed. You or use a navigationGuard to resolve the API during the route execution. This functionality is asynchronous, so you can await the response before proceeding.
Alternatively, if you really want to handle it in the component, you will have that delay while the API is resolving, but you can implement some loader animation to improve the experience.

Vue - Best way to poll the same API endpoint for multiple components simultaneously

I need to fetch continuously updating API endpoint data for several components on my NUXT site, 2 of which are visible simultaneously at any given moment. I wonder what is the best practice to poll the same endpoint for several different components visible at the same time?
Currently, I am using VUEX to send the API data to my components and then using setInterval with a refresh function to update the data in each component. This is clearly a clumsy solution (I am a beginner). I was thinking about polling the API directly in VUEX but I understand this is not advisable either.
This is my current (clumsy) solution:
VUEX:
// STATE - Initial values
export const state = () => ({
content: {}
});
// ACTIONS
export const actions = {
async nuxtServerInit ({ commit }) {
const response = await this.$axios.$get('https://public.radio.net/stations/see15310/status');
commit('setContent', response)
}
}
// MUTATIONS
export const mutations = {
setContent(state, content) {
state.content = content;
}
}
And in each component:
computed: {
content () {
return this.$store.state.content
},
methods: {
refresh() {
this.$nuxt.refresh()
}
},
mounted() {
window.setInterval(() => {
this.refresh();
}, 5000);
I think, it's a normal solution to do the polling in vuex. Vuex is your application state and the one source of truth for all dependant components. And if you need to update some similar state for different components - it's a rational solution to do it in vuex action.
Another solution could be the event bus. First article about it from google
Also, I don't recommend use SetInterval for polling. Because it's don't wait of async operation ending. This fact could shoot you in the foot, if a client has network delay or another glitch. I used SetTimeout for this purpose.
async function getActualData() {
// get data from REST API
}
async function doPolling() {
const newData = await getActualData() // waiting of finish of async function
// update vuex state or send update event to event bus
setTimeout(doPolling, 5000)
}
doPolling()
If my answer missed into your question, then give me more details please. What is the problem you want to solve? And what disadvantages do you see in your(by your words ;) ) "clumsy" solution?

How to use async/await in vue lifecycle hooks with vuex?

When I dispatch an action in App.vue component in mounted() lifecycle hook, it runs after other components load. I am using async/await in my action and mounted lifecycle hook.
App.vue file
methods: {
...mapActions({
setUsers: "setUsers",
}),
},
async mounted() {
try {
await this.setUsers();
} catch (error) {
if (error) {
console.log(error);
}
}
},
action.js file:
async setUsers(context) {
try {
const response = await axios.get('/get-users');
console.log('setting users');
if (response.data.success) {
context.commit('setUsers', {
data: response.data.data,
});
}
} catch (error) {
if (error) {
throw error;
}
}
},
In Users list component, I need to get users from vuex. So I am using mapGetters to get Users list.
...mapGetters({
getUsers: "getUsers",
}),
mounted() {
console.log(this.getUsers);
},
But the problem is "setting users" console log in running after console logging the this.getUsers.
In Users list component, I can use getUsers in the template but when I try to console log this.getUsers it gives nothing.
How can I run app.vue file before running any other components?
You are using async await correctly in your components. It's important to understand that async await does not hold off the execution of your component, and your component will still render and go through the different lifecycle hooks such as mounted.
What async await does is hold off the execution of the current context, if you're using it inside a function, the code after the await will happen after the promise resolves, and in your case you're using it in the created lifecycle hook, which means that the code inside the mounted lifecycle hook which is a function, will get resolved after the await.
So what you want to do, is to make sure you render a component only when data is received.
Here's how to do it:
If the component is a child component of the parent, you can use v-if, then when the data comes set data to true, like this:
data() {
return {
hasData: false,
}
}
async mounted() {
const users = await fetchUsers()
this.hasData = true;
}
<SomeComponent v-if="hasData" />
If the component is not a child of the parent, you can use a watcher to let you know when the component has rendered. When using watch you can to be careful because it will happen every time a change happens.
A simple rule of thumb is to use watch with variables that don't change often, if the data you're getting is mostly read only you can use the data, if not you can add a property to Vuex such as loadingUsers.
Here's an example of how to do this:
data: {
return {
hasData: false,
}
},
computed: {
isLoading() {
return this.$store.state.app.users;
}
},
watch: {
isLoading(isLoading) {
if (!isLoading) {
this.hasData = true;
}
}
}
<SomeComponent v-if="hasData" />
if you're fetching a data from an API, then it is better to dispatch the action inside of created where the DOM is not yet rendered but you can still use "this" instead of mounted. Here is an example if you're working with Vuex modules:
created() {
this.fetchUsers();
},
methods: {
async fetchUsers() {
await this.$store.dispatch('user/setUsers');
},
},
computed: {
usersGetters() {
// getters here
},
},
Question: Do you expect to run await this.setUsers(); every time when the app is loaded (no matter which page/component is being shown)?
If so, then your App.vue is fine. And in your 'Users list component' it's also fine to use mapGetters to get the values (note it should be in computed). The problem is that you should 'wait' for the setUsers action to complete first, so that your getUsers in the component can have value.
A easy way to fix this is using Conditional Rendering and only renders component when getUsers is defined. Possibly you can add a v-if to your parent component of 'Users list component' and only loads it when v-if="getUsers" is true. Then your mounted logic would also work fine (as the data is already there).

Can I cache an API call object using Vue JS lifecycle hooks?

I'm using The Strain API which has a search query that gets all the strains in its database. It says to use this sparingly as it requires a lot of computing power. My question is: can I call it once in the App component life cycle hook, save the response in the App data(), and cache it somehow so that if the data.object.length != 0, it doesn't try calling it again? Then I could pass it as a prop to any other component that needs it. Sorry, new to VueJs and programming.
the best thing in my opinion is to keep the calculated data in a service, so that you in your mounted component simply call the getData (), imported from the StrainService.js
// StrainService.js
let response = null;
const getData = async () => {
if (!response) {
response = await StrainApiCall()
}
return response
}
const StrainApiCall = () => {
return axios.get('yourdata.com') // your api call
}
export {
getData
}

using async data in my page using nuxtjs

I have read using async data or fetch is a better approach in pages in nuxtjs instead of using the created hook.
I am struggling to get my code to work though
I had the following (Which does work fine)
created () {
this.$store.dispatch('cases/getCase', this.$route.params.caseId );
},
But how would I change that to work with the async method instead please, and be able to return more than one state when I need to.
I tried the following
async asyncData ({ params }) {
const thisCase = await this.$store.dispatch('cases/getCase', this.$route.params.caseId );
// constant thisUser
return { thisCase }
// return { thisCase, thisUser}
},
but this generated an error
undefined is not an object (evaluating 'this.$store')
Can anyone tell me what I am doing wrong please
Thanks
this not available in asyncData/fetch. It is even stated in docs in special orange warning.
You do NOT have access of the component instance through this inside
asyncData because it is called before initiating the component.
And again as said in docs
method receives the context object as the first argument, you can use
it to fetch some data and return the component data.
Context is where from you should be getting your store. Here docs for context.
So your code would be
async asyncData ({ params, store }) {
const thisCase = await store.dispatch('cases/getCase', params.caseId )
return { thisCase }
},