I'm having an issue with the initial state of data in my application. I'm using vuex and vue-router, and I think the async stuff is tripping me up, but I'm not sure how to fix it.
In my view.vue component:
beforeRouteEnter(to, from, next) {
store.dispatch('assignments/getAssignment', {
id: to.params.id
}).then(res => next());
},
In my module:
getAssignment({commit, state}, {id}) {
return axios.get('/assignments/' + id)
.then(response => {
if(response.data.data.type == 'goal_plan') {
const normalizedEntity = normalize(response.data.data, assignment_schema);
commit('goals/setGoals', {goals: normalizedEntity.entities.goals}, {root: true});
commit('goals/setGoalStrategicPriorities', {goal_priorities: normalizedEntity.entities.strategicPriorities}, {root: true});
commit('goals/setObjectives', {objectives: normalizedEntity.entities.objectives}, {root: true});
commit('goals/setStrategies', {strategies: normalizedEntity.entities.strategies}, {root: true});
}
commit('setAssignment', {assignment: response.data.data});
}).catch(error => {
console.log(error);
EventBus.$emit('error-thrown', error);
});
},
A couple of subcomponents down, I want to access state.goals.goals, but it is initially undefined. I can handle some of the issues from that, but not all.
For example, I have a child component of view.vue that includes
computed: {
originalGoal() {
return this.$store.getters['goals/goalById'](this.goalId);
},
},
data() {
return {
form: {
id: this.originalGoal.id,
description: this.originalGoal.description,
progress_type: this.originalGoal.progress_type,
progress_values: {
to_reach: this.originalGoal.progress_values.to_reach,
achieved: this.originalGoal.progress_values.achieved,
},
due_at: moment(this.originalGoal.due_at).toDate(),
status: this.originalGoal.status,
},
In the heady days before I started using vuex, I was passing in the original goal as a prop, so it wasn't an issue. Since it's now pulled from the state, I get a bunch of errors that it can't find the various properties of undefined. Eventually originalGoal resolves in the display, but it's never going to show up in the form this way.
I tried "watch"ing the computed prop, but I never saw when it changed, and I'm pretty sure that's not the right way to do it anyway.
So, is there a way to get the data set initially? If not, how should I go about setting the form values once the data IS set? (Any other suggestions welcome, as I'm pretty new to vuex and vue-router.)
So if I set the form values in "mounted," I'm able to get this to work. Still learning about the vue life-cycle I guess. :)
Related
I need help. I'm kind of an amateur in Vue3, and can´t understand why this happens:
If I set this in the parent component:
props: [ 'code' ],
data() {
return {
asset: {
id: '',
brand: '',
group: {
name: '',
area: ''
}
}
}
},
created() {
axios.get('/api/myUrl/' + this.code, {})
.then(response => {
if (response.status === 200) {
this.asset = response.data;
}
})
.catch(error => {
console.log(error);
})
}
then, in my component <asset-insurance :asset_id="asset.id"></asset-insurance>, asset_id prop is empty.
But, if I set:
props: [ 'code' ],
data() {
return {
asset: []
}
},
created() {
axios.get('/api/myUrl/' + this.code, {})
.then(response => {
if (response.status === 200) {
this.asset = response.data;
}
})
.catch(error => {
console.log(error);
})
}
then, the asset_id prop gets the correct asset.id value inside <asset-insurance> component, but I get a few warnings and an error in the main component about asset property group.name not being set (but it renders correctly in template).
Probably I'm doing something terribly wrong, but I can't find where is the problem. Any help?
Edit:
I'm checking the prop in child component AssetInsurance just by console.logging it
<script>
export default {
name: "AssetInsurance",
props: [
'asset_id'
],
created() {
console.log(this.asset_id)
}
}
</script>
asset_id is just an integer, and is being assigned correctly in parent's data asset.id, because I'm rendering it in the parent template too.
It's incorrect to define asset as an array and reassign it with plain object. This may affect reactivity and also prevents nested keys in group from being read.
The problem was misdiagnosed. asset_id value is updated but not at the time when this is expected. Prop value is not available at the time when component instance is created, it's incorrect to check prop value in created, especially because it's set asynchronously.
In order to output up-to-date asset_id value in console, a watcher should be used, this is what they are for. Otherwise asset_id can be used in a template as is.
Ok, I think I found the proper way, at least in my case.
Prop is not available in the component because it is generated after the creation of the component, as #EstusFlask pointed.
The easy fix for this is to mount the component only when prop is available:
<asset-insurance v-if="asset.id" :asset_id="asset.id"></asset-insurance>
This way, components are running without problem, and I can declare objects as I should. :)
Thank you, Estus.
I want to trigger a function that GETs data from a http-server in a component, as soon as a button in a sibling component was pressed.
SignUpForm.vue has a button that triggers customSubmit()
customSubmit(){
//POST to API
const user = {
method: "POST",
headers: { "Content-Type": "application/json"},
body: JSON.stringify({newUser: this.newUser})
};
fetch("http://localhost:3080/api/user", user)
.then(response => response.json())
.then(data => console.log(data));
this.$emit('refresh', true)
this.clearForm();
}
The parent component looks as follows:
<template>
<div>
<SignUpForm #refresh="triggerRefresh($event)" />
<!-- <Exp /> -->
<Datatable :myRefresh="myRefresh" />
</div>
</template>
<script>
import SignUpForm from "./components/SignUpForm.vue";
import Datatable from "./components/Datatable.vue";
import Exp from "./components/exp copy.vue";
export default {
name: "App",
components: { Datatable, SignUpForm, Exp },
data() {
return {
myRefresh: false,
};
},
methods: {
triggerRefresh(bool) {
this.myRefresh = bool;
console.log(this.myRefresh);
},
},
};
</script>
Now i want the sibling component Datatable.vue
to fetch data from the server as soon, as this.$emit('refresh', true) is fired in SignUpForm.vue
Here's the script from Datatable.vue
export default {
data() {
return {
//Liste aller User
userData: null,
//temporärer User für das Details-Feld
printUser: [{ name: "", email: "", number: "" }],
//Property für den "read-Button"
showDetails: false,
//Property für den "Update-Button"
readOnly: true,
};
},
props: ["myRefresh"],
methods: {
pushFunction() {
fetch("http://localhost:3080/api/users")
.then((res) => res.json())
.then((data) => (this.userData = data));
},
readData(k) {
this.printUser.length = 0;
this.showDetails = true;
this.printUser.push(this.userData[k]);
},
editData(rowUser) {
if (!rowUser.readOnly) {
rowUser.readOnly = true;
const user = {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ userData: this.userData }),
};
fetch("http://localhost:3080/api/users/patch", user)
.then((response) => response.json())
.then((data) => console.log(data));
} else {
rowUser.readOnly = false;
}
},
deleteData(k) {
fetch("http://localhost:3080/api/users/" + k, { method: "DELETE" }).catch(
(err) => console.log(err)
);
this.pushFunction();
},
//blaue Reihen
toggleHighlight(rowUser) {
if (rowUser.readOnly === false) {
return;
}
rowUser.isHighlight = !rowUser.isHighlight;
},
scrollDown() {
window.scrollTo(0, document.body.scrollHeight);
},
},
mounted() {
fetch("http://localhost:3080/api/users")
.then((res) => res.json())
.then((data) => (this.userData = data));
},
};
I really hope somebody can help a newbie out!
Two considerations, is it possible? and is it prudent?
Is it possible?
Yes, it is and you can implement it couple different ways.
Is it prudent?
No.
If you're going down this road, most likely the architecture is ineffective. In an ideal setup, your components should be responsible for managing the view only. That means what the user sees and collecting their input. The business logic should not live in the components. So if you have things like ajax calls and you put them into your component, you've coupled the logic to the view. One possible issue is that if the component is re-added for some reason, any in-progress ajax calls could be disrupted in an unexpected manner. While such scenarios can be handled, the bigger issue IMHO is that that when you are coupling business logic with the view layer you are creating an application that becomes increasingly difficult to reason about; This problem you have with sending event between sibling components is just one example.
Other options
The most common way, though not the only way, of dealing with this is by using a global store via Vuex.
Instead of initializing the Ajax request from your component, you call the Vuex action.
The action would usually set loading state either using single state variable (ie loadState=STATE.STARTED) or using isLoading=true, except instead of assigning the variable, vuex would do it through a mutation, so store.commit('setLoadState', STATE.LOADING). this will update the state in all components that are listening for changes in either the store directly or using a getter. Then the ajax request is made, and when it is done the store is updated again, either with store.commit('setLoadState', STATE.ERROR) or on success, store.commit('setLoadState', STATE.DONE) and store.commit('setUsers', response). Then your components only need to listen for changes, you can display a spinner if $store.loadState == STATE.LOADING
As long as the data for the subsequent call is related to data specific to the component (like specific user ID or name) you can handle the next call from the component. Instead of triggering the second API request from the component by watching for an event from the sibling, you can have the component watch the vuex store or data for a change. Then when $store.loadState becomes STATE.DONE, you can trigger another action for the other API call. I would only do this though if there is any part of the data that is specific to the API call, otherwise if the call comes right after in all circumstances, you might as-well call it as part of the same action
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).
I use vuex for my state as well as fetching data and display it in my application.
But I wonder if I'm doing it right. At the moment I dispatch an fetchDataAsync action from the component mounted hook, and I have an getter to display my data. Below is a code example of how I do it currently.
I wonder if it's necessary. What I really want is a getter, that looks at the state, checks if the data is already there and if the data is not there it is able to dispatch an action to fetch the missing data.
The API of vuex does not allow it so I need to put more logic into my components. E.g. if the data is depended of a prop I need a watcher that looks at the prop and dispatches the fetchDataAsync action.
For me it just feels wrong and I wonder if there is a better way.
let store = new Vuex.Store({
state: {
posts: {}
},
mutations: {
addPost(state, post) {
Vue.set(state.posts, post.id, post);
}
},
actions: {
fetchPostAsync({ commit }, parameter) {
setTimeout(
() =>
commit("addPost", { id: parameter, message: "got loaded asynchronous" }),
1000
);
}
},
getters: {
// is it somehow possible to detect: ob boy, I don't have this id,
// I'd better dispatch an action trying to fetch it...?
getPostById: (state) => (id) => state.posts[id]
}
});
new Vue({
el: "#app",
store,
template : "<div>{{ postToDisplay ? postToDisplay.message : 'loading...' }} </div>",
data() {
return {
parameter: "a"
};
},
computed: {
...Vuex.mapGetters(["getPostById"]),
postToDisplay() {
return this.getPostById(this.parameter);
}
},
methods: {
...Vuex.mapActions(["fetchPostAsync"])
},
mounted() {
this.fetchPostAsync(this.parameter);
}
});
I also created a codepen
Personally I think the solution you suggested (adding a watcher that dispatches fetchPostAsync if the post is not found) is the best one. As another commenter stated, getters should not have side effects.
In my parent component:
<UsersList :current-room="current_room" />
In the child component:
export default {
props: {
currentRoom: Object
},
data () {
return {
users: []
}
},
mounted () {
this.$nextTick( async () => {
console.log(this.currentRoom) // this, weirdly, has the data I expect, and id is set to 1
let url = `${process.env.VUE_APP_API_URL}/chat_room/${this.currentRoom.id}/users`
console.log(url) // the result: /api/chat_room/undefined/users
let response = await this.axios.get(url)
this.users = response.data
})
},
}
When I look at the page using vue-devtools, I can see the data appears:
I've run into this issue in the past – as have many others. For whatever reason, you can't rely on props being available in the component's mounted handler. I think it has to do with the point at which mounted() is called within Vue's lifecycle.
I solved my problem by watching the prop and moving my logic from mounted to the watch handler. In your case, you could watch the currentRoom property, and make your api call in the handler:
export default {
props: {
currentRoom: Object
},
data() {
return {
users: []
}
},
watch: {
currentRoom(room) {
this.$nextTick(async() => {
let url = `${process.env.VUE_APP_API_URL}/chat_room/${room.id}/users`
let response = await this.axios.get(url)
this.users = response.data
})
}
},
}
I don't think you really need to use $nextTick() here, but I left it as you had it. You could try taking that out to simplify the code.
By the way, the reason console.log(this.currentRoom); shows you the room ID is because when you pass an object to console.log(), it binds to that object until it is read. So even though the room ID is not available when console.log() is called, it becomes available before you see the result in the console.