How to wait for state change before executing next action in Vuex - vue.js

I'm trying to login and get user name at the same time. Here is how it works:
First action login gets access token and updates state.accessToken
Using state.accessToken I need to get user data at the same time (fetchUser) when login is pressed, however when fetchUser is executed state.accessToken is still null as actions are asynchronous. What is the best practice to wait for state change before executing the next action? I tried to look up for examples, but solutions found are not applicable for my case.
store.js
const store = new Vuex.Store({
state: {
accessToken: null,
user: null
},
mutations: {
authUser (state, userData) {
state.accessToken = userData.accessToken
},
storeUser (state, user) {
state.user = user
}
}
actions: {
login({commit}, authData) {
axios.post("http://example.com/token/create/", {
email: authData.email,
password: authData.password
})
.then(res => {
commit('authUser', {
accessToken: res.data.access
})
})
},
fetchUser({commit, state}) {
axios.get("http://example.com/api/auth/v1/me/", {
headers: {Authorization: "Bearer " + state.accessToken}
})
.then(res => {
commit('storeUser', res.data.user)
})
}
}
getters: {
user (state) {
return state.user
},
isAuthenticated(state) {
return state.accessToken !== null
}
}
})
login.vue
<template>
<form #submit.prevent="submitForm">
<div v-if="!auth" class="row">
<input class="col" placeholder="Email" v-model="formInfo.email" type="text"></input>
<input class="col" placeholder="Password" v-model="formInfo.password" type="password"></input>
<button class="col" type="submit" label="Log In"></button>
</div>
<div v-else class="row">
Hello {{ firstname }}
</div>
</form>
</template>
<script>
export default {
data() {
return {
formInfo: {
email: '',
password: ''
}
};
},
methods: {
submitForm() {
this.$store.dispatch('login', {email: this.formInfo.email, password: this.formInfo.password})
this.$store.dispatch('fetchUser')
}
},
computed: {
auth() {
return this.$store.getters.isAuthenticated
},
firstname() {
return this.$store.getters.user.firstname
}
}
}
};
</script>

have you tried adding await?
async submitForm() {
await this.$store.dispatch('login', {email: this.formInfo.email, password: this.formInfo.password});
await this.$store.dispatch('fetchUser');
}
it will wait for the login to finish before fetching the user
also maybe add async in your actions methods:
async login({commit}, authData) {...}
async fetchUser({commit, state}) {...}

You can dispatch an action from another action.
actions: {
login({commit, dispatch}, authData) {
axios.post("http://example.com/token/create/", {
email: authData.email,
password: authData.password
})
.then(res => {
const accessToken = res.data.access;
dispatch('fetchUser', accessToken);
commit('authUser', { accessToken })
})
},
fetchUser({commit, state}, accessToken) {
axios.get("http://example.com/api/auth/v1/me/", {
headers: {Authorization: "Bearer " + accessToken}
})
.then(res => {
commit('storeUser', res.data.user)
})
}
}

Evan's async/await solution should work.
If there are multiple pages/components that can issue actions in any order, and you need to not proceed with one action before another is complete, you could use the strategy of storing promises as state. You can then access the promise any other action depends on the completion of and await it before proceeding.
So in login, you would use async/await syntax and an extra mutation call:
const promise = axios.post("http://example.com/token/create/", {
email: authData.email,
password: authData.password
});
commit('loginPromise', promise);
const result = await promise;
You would need to add the above mutation.
Then in fetchUser, before issuing the API call:
await state.loginPromise
That would guarantee that the promise has completed. You will have to handle error scenarios of course.

Related

vue component not displaying computed properties

This is a Vue3 project. When Domains.vue is mounted, getDomains is dispatched to vuex, and the data is properly set as indicated by vue dev tools.
For some reason, the data is not displayed in the template for loop. Perhaps one of you wonderful people can help me figure out why not?
Domains.vue
<template>
<div class="domains">
<h1>This is an domains page</h1>
<ul>
<li v-for="item in domains" :key="item.post_name">
<h3>{{ item.post_title }}</h3>
<p>{{ item.post_excerpt }}</p>
</li>
</ul>
</div>
</template>
<script>
export default {
name: 'Domains',
computed: {
domains() {
return this.$store.state.domains.domains
},
},
mounted() {
this.$store.dispatch('getDomains')
}
}
</script>
vuex store
import { createStore } from 'vuex'
import axios from 'axios'
export default createStore({
state: {
user: {
'id': localStorage.getItem('id'),
'token': localStorage.getItem('token'),
},
domains: {
domains: [],
totalDomains: '',
totalPages: ''
},
},
mutations: {
SET_USER(state, user) {
state.user = user
localStorage.setItem('id', user.id)
localStorage.setItem('token', user.token)
},
DELETE_USER(state) {
state.user = { token: '' }
localStorage.setItem('id', '')
localStorage.setItem('token', '')
},
SET_DOMAINS(state, data, headers) {
state.domains.domains = data
state.domains.totalDomains = headers['X-WP-Total']
state.domains.totalDomains = headers['X-WP-TotalPages']
},
SET_ME(state, data) {
state.user.me = data
},
},
actions: {
login({ commit }, payload) {
return new Promise(async (resolve, reject) => {
try {
const { data } = await axios.post(`http://sslchkr.com/wp-json/jwt-auth/v1/token`, payload)
commit('SET_USER', data)
resolve(data)
} catch(e) {
reject(e)
}
})
},
logout({ commit }) {
commit('DELETE_USER')
},
validate({ state }) {
return new Promise(async (resolve, reject) => {
try {
const { data } = await axios({
url: `http://sslchkr.com/wp-json/jwt-auth/v1/token/validate`,
method: 'post',
headers: {
'Authorization': `Bearer ${state.user.token}`
}
})
//commit('SET_USER', data)
resolve(data)
} catch(e) {
reject(e)
}
})
},
getDomains({ commit, state }) {
return new Promise(async (resolve, reject) => {
try {
const { data, headers } = await axios.get(`http://sslchkr.com/wp-json/sslchkr/v1/author/${state.user.id}/domain`, {
headers: {
Authorization: `Bearer ${state.user.token}`
}
})
commit('SET_DOMAINS', data, headers)
resolve(data)
} catch(e) {
reject(e)
}
})
},
getMe({ commit, state }) {
return new Promise(async (resolve, reject) => {
try {
const { data } = await axios.get(`http://sslchkr.com/wp-json/wp/v2/users/me`, {
headers: {
Authorization: `Bearer ${state.user.token}`
}
})
commit('SET_ME', data)
resolve(data)
} catch(e) {
reject(e)
}
})
},
},
modules: {
}
})
convert this
<li v-for="item in domains" :key="item.post_name">
to
<li v-for="item in domains" :key="item">
and if this doesn't work, add index as key
<li v-for="(item,idx) in domains" :key="idx">
Please disregard this. I jumped the gun and posted the question before I knew what was wrong.

How can I update the comments without refreshing it?

First, I'm using vuex and axios.
store: commentService.js
components:
CommentBox.vue (Top components)
CommentEnter.vue (Sub components)
This is the logic of the code I wrote.
In the store called commentService.js, there are mutations called commentUpdate.
And There are actions called postComment and getComment.
At this time, In the component called CommentBox dispatches getComment with async created().
Then, in getComment, commentUpdate is commited and executed.
CommentUpdate creates an array of comments inquired by getComment and stores them in a state called commentList.
Then I'll get a commentList with "computed".
CommentEnter, a sub-component, uses the commentList registered as compounded in the CommentBox as a prop.
The code below is commentService.js.
import axios from 'axios'
export default {
namespaced: true,
state: () => ({
comment:'',
commentList: []
}),
mutations: {
commentUpdate(state, payload) {
Object.keys(payload).forEach(key => {
state[key] = payload[key]
})
}
},
actions: {
postComment(state, payload) {
const {id} = payload
axios.post(`http://??.???.???.???:????/api/books/${id}/comments`, {
comment: this.state.comment,
starRate: this.state.starRate
}, {
headers: {
Authorization: `Bearer ` + localStorage.getItem('user-token')
}
})
.then((res) => {
console.log(res)
this.state.comment = ''
this.state.starRate = ''
)
.catch((err) => {
alert('댓글은 한 책당 한 번만 작성할 수 있습니다.')
console.log(err)
this.state.comment = ''
this.state.starRate = ''
})
},
async getComment({commit}, payload) {
const {id} = payload
axios.get(`http://??.???.???.???:????/api/books/${id}/comments`)
.then((res) => {
console.log(res)
const { comment } = res.data.commentMap
commit('commentUpdate', {
commentList: comment
})
})
.catch((err) => {
console.log(err)
commit('commentUpdate', {
commentList: {}
})
})
}
}
}
The code below is CommentBox.vue
computed: {
commentList() {
return this.$store.state.commentService.commentList
}
},
methods: {
async newComment() {
if(this.$store.state.loginService.UserInfoObj.id === '') {
alert('로그인 후 이용할 수 있습니다.')
return
}
this.$store.dispatch('commentService/postComment', {
id: this.$route.params.id,
comment: this.$store.state.comment,
starRate: this.$store.state.starRate
})
}
},
async created() {
this.$store.dispatch('commentService/getComment', {
id: this.$route.params.id
})
}
The code below is CommentEnter.vue
created() {
this.userComment = this.comment
},
props: {
comment: {
type: Object,
default: () => {}
}
},
I asked for a lot of advice.
There were many comments asking for an axios get request after the axios post request was successful.
In fact, I requested an axios get within .then() of the axios post, and the network tab confirmed that the get request occurred normally after the post request.
But it's still not seen immediately when I register a new comment.
I can only see new comments when I refresh it.
How can I make a new comment appear on the screen right away when I register it?
Can't you just call getComment when postComment is finished?
methods: {
async newComment() {
if(this.$store.state.loginService.UserInfoObj.id === '') {
alert('로그인 후 이용할 수 있습니다.')
return
}
this.$store.dispatch('commentService/postComment', {
id: this.$route.params.id,
comment: this.$store.state.comment,
starRate: this.$store.state.starRate
}).then(function() {
this.$store.dispatch('commentService/getComment', {
id: this.$route.params.id
})
})
}
},
}
Or since you're using async:
methods: {
async newComment() {
if(this.$store.state.loginService.UserInfoObj.id === '') {
alert('로그인 후 이용할 수 있습니다.')
return
}
await this.$store.dispatch('commentService/postComment', {
id: this.$route.params.id,
comment: this.$store.state.comment,
starRate: this.$store.state.starRate
})
this.$store.dispatch('commentService/getComment', {
id: this.$route.params.id
})
}
},
}

Copy of store not updated when mounted Async axios

I have been struggling with this issue for a day now. I want to make a copy of the store for user into userCopy so that it can be edited by the user without causing a mutation. My problem is that even though I am using the mounted hook, userCopy only returns an empty store state.
pages/settings/_id.vue
<template>
<div>
{{ user }} // will display the whole object
{{ userCopy }} // will only display empty store object
</div>
</template>
<script>
import { mapState } from 'vuex'
import _ from 'lodash'
data() {
return {
userCopy: {}
}
},
computed: {
...mapState({ user: (state) => state.staff.user })
},
created() {
this.$store.dispatch('staff/fetchUser', this.$route.params.id)
},
mounted() {
this.$data.userCopy = _.cloneDeep(this.$store.state.staff.user)
},
</script>
store/staff.js
import StaffService from '~/services/StaffService.js'
export const state = () => ({
user: {
offers: '',
legal: ''
}
})
export const mutations = {
SET_USER(state, user) {
state.user = user
},
}
export const actions = {
fetchUser({ commit, getters }, id) {
const user = getters.getUserById(id)
if (user) {
commit('SET_USER', user)
} else {
StaffService.getUser(id) // StaffService users axios get call
.then((response) => {
commit('SET_USER', response.data)
})
.catch((error) => {
console.log('There was an error:', error.response)
})
}
},
}
export const getters = {
getUserById: (state) => (id) => {
return state.staff.find((user) => user.id === id)
}
}
Even using this mounted method did not solve the issue. The userCopy object still returns empty.
mounted() {
this.$store
.dispatch('staff/fetchUser', this.$route.params.id)
.then((response) => {
this.userCopy = this.$store.state.staff.user
})
},
It seems that the mounted() is called before your network request get solved.
To fix this, I suggest to do like this.
First:
if (user) {
console.log('user found',user)
commit('SET_USER', user)
return user
} else {
console.log('user not found')
//RETURN the Axios Call here
return StaffService.getUser(id) // StaffService users axios get call
.then((response) => {
commit('SET_USER', response.data)
//return the response here, after committing
return response.data
})
then in your component
mounted() {
this.$store
.dispatch('staff/fetchUser', this.$route.params.id)
.then((response) => {
console.log(response)
this.userCopy = response
})
}

Get UserName and Make UserName reactive after Vue Authentication

I am having trouble getting user data and making user data reactive after the user has logged In.
Without using Store i am getting the user information but I am unable to make it reactive. So I tried storing user information in store. Now I am having getting that data as well.
I have a login form in LOGINCOMPONENT.VUE that has two input fields email and password.
<form #submit.prevent="login">
<input placeholder="Email" type="email" v-model="formData.email">
<input placeholder="Password" type="password" v-model="formData.password">
</form>
Script portion:
export default {
name: 'LoginPage',
data() {
return {
formData: {},
};
},
methods: {
login() {
this.$axios.post('login', this.formData).then(async (res) => {
await localStorage.setItem('user', JSON.stringify(res));
await localStorage.setItem('token', res.token);
this.$router.push('/');
console.log(res);
this.$store.dispatch('userDataAction', res); --->>> Using Store to take user data
}).catch((error) => {
console.log(error);
});
},
},
};
Login process goes well and user token is generated.
This is my store.
const state = {
token: localStorage.getItem('token') || null,
userData: {},
};
const getters = {
getUserData: state => state.userData,
loggedIn: state => state.token != null,
};
const mutations = {
userDataMutation(state, userData) {
state.userData = userData;
},
};
const actions = {
userDataAction(context, credentials) {
const userData = {
username: credentials.username,
email: credentials.email,
firstName: credentials.first_name,
lastName: credentials.last_name,
};
context.commit('userDataMutation', userData);
},
};
Finally in my HEADERCOMPONENT.VUE where i am showing "SIGN IN" if user is not logged In and "HELLO USERNAME" if user is logged in.
export default {
name: 'HeaderComponent',
computed: {
...mapGetters(['getUserData', 'loggedIn']),
},
};
Template:
<div> {{ loggedIn ? getUserData.username : 'Sign In' }} </div>

Triggering a route only after dispatch and commit completed in VueJS

I do have a form submit which takes email and password then pass them into an action in store called userSignIn
SignIn.vue :
onSubmit () {
if (this.$refs.form.validate()) {
const user = {
email: this.email,
password: this.password
}
this.$store.dispatch('userSignIn', user)
.then(() => {
this.$router.push('/')
}).catch(err => {
console.log(err)
})
}
}
Within store, I do have a userSignIn action like this
store.js actions:
userSignIn ({commit, getters}, payload) {
getters.Api.post(`user/signin`, {
email: payload.email,
password: payload.password
}).then(res => {
commit('userSignIn', res.data.token)
}).catch(err => {
console.log(err)
})
}
The routing(this.$router.push('/')) should only be done after userSignIn commit(commit('userSignIn', res.data.token)). But what actually happening routing triggers before commit, which results and error because user token is not set yet.
How to trigger something(in this case this.$router.push('/')) only after completion of dispatch and commit within it?
Returning the promise did the trick.
userSignIn ({commit, getters}, payload) {
return getters.Api.post(`user/signin`, {
......
})