I have to following situation with vue2/vuex; Let's say I have a users module where I store all users I fetched from my api.
I use this module to populate, for example, dropdown lists containing all users.
Now I also have a users page, but this page has the option to filter, paginate users etc. This happens serverside, so the module will be updated with the new list of (filtered) users.
Should I create two separate modules for both usecases (usersOptions and usersView)? To me it would seem more logical to create two instances of the user store, but apparently that's not possible with Vuex. How would you handle a situation like this?
Here is an example of the my users module:
import UserRepository from '#/repositories/UserRepository';
export default {
namespaced: true,
state: {
loading: false,
users: [],
},
getters: {
isLoading(state) {
return state.loading;
},
data(state) {
return state.users;
},
options: (state) => (value = 'id', label = 'name') => state.users.map(
(user) => ({ value: user[value], label: user[label] }),
),
},
mutations: {
SET_LOADING(state, payload) {
state.loading = payload;
},
SET_DATA(state, payload) {
state.users = payload;
},
},
actions: {
fetch({ commit }) {
return new Promise((resolve, reject) => {
commit('SET_LOADING', true);
UserRepository.index({ limit: 0 })
.then((response) => {
const users = response.data.data;
commit('SET_DATA', users);
resolve(response);
})
.catch((error) => {
reject(error);
})
.finally(() => {
commit('SET_LOADING', false);
});
});
},
},
};
Intuitively, I'd do something like that. Haven't tested it but it's probably not ready to use yet.
import UserRepository from '#/repositories/UserRepository';
export default {
namespaced: true,
state: {
loading: false,
users: [],
},
getters: {
isLoading(state) {
return state.loading;
},
data(state) {
return state.users;
},
usersView() {
return state.users.view;
},
usersOptions() {
return state.users.options;
},
options: (state) => (value = 'id', label = 'name') => state.users.map(
(user) => ({ value: user[value], label: user[label] }),
),
},
mutations: {
SET_LOADING(state, payload) {
state.loading = payload;
},
SET_DATA(state, key, payload) {
state.users[key] = payload;
},
},
actions: {
fetch({ commit }, params) {
return new Promise((resolve, reject) => {
commit('SET_LOADING', true);
UserRepository.index(params)
.then((response) => {
resolve(response.data.data);
})
.catch((error) => {
reject(error);
})
.finally(() => {
commit('SET_LOADING', false);
});
});
},
fetchOptions() {
this.dispatch('fetch', { limit: 0 }).then((users) {
commit('SET_DATA', 'options', users);
})
},
fetchView() {
this.dispatch('fetch', { limit: 15, page: 1 }).then((users) {
commit('SET_DATA', 'view', users);
})
},
},
};
Two stores its never the soultion in my opinion,
try to seperate to 2 modules.
find more here: https://vuex.vuejs.org/guide/modules.html
Related
I can't seem to pass data to my Vuex action. I always get the error message TypeError: can't access property x, data is undefined. I have done a few different approaches on passing property value but I couldn't make it work. Can you look at my code, what am I doing wrong?
Store:
import axios from 'axios'
export default {
namespaced: true,
state: {
announcements: []
},
getters: {},
actions: {
createAnnouncement({ commit }, courseId, data) {
return new Promise((resolve, reject) => {
axios
.post(`teacher/courses/announcements/create/${courseId}`, { title: data.title, message: data.message })
.then((response) => {
commit('ADD_ANNOUNCEMENT', response.data.data)
resolve(response)
})
.catch((error) => { reject(error) })
})
}
},
mutations: {
ADD_ANNOUNCEMENT(state, newAnnouncement) {
state.announcements.push(newAnnouncement)
}
}
}
Component:
<script>
import { mapActions, mapGetters } from 'vuex'
export default {
data() {
return {
title: '',
message: '',
}
},
computed: {
...mapGetters('courses', ['getCourseData'])
},
methods: {
...mapActions('announcements', ['createAnnouncement']),
onSubmit() {
const { announcementTitle, announcementMessage } = this
const courseId = this.getCourseData.id
this.createAnnouncement(courseId, { title: announcementTitle, message: announcementMessage })
}
}
}
</script>
Actions only accept two arguments, the context and an optional payload.
Try changing your action to
createAnnouncement({ commit }, { courseId, data }) {
//...
}
and dispatch it with
const payload = {
courseId: this.getCourseData.id,
data: {
title: this.announcementTitle,
message: this.announcementMessage
}
}
this.createAnnouncement(payload)
See https://vuex.vuejs.org/api/#actions
I've created simple VueCLI auth module using axios and Vuex.
In store.js I've got all logic for tokens using api from session.js:
import Vue from 'vue'
import Vuex from 'vuex'
import axios from 'axios'
import sessionSerivce from '#/services/session.js'
Vue.use(Vuex)
Vue.use(require('vue-cookies'))
export const store = new Vuex.Store({
state: {
status: '',
accessToken: $cookies.get('accessToken') || '',
refreshToken: $cookies.get('refreshToken') || '',
user: $cookies.get('user') || '',
},
actions: {
login({ commit }, data) {
return new Promise((resolve, reject) => {
commit('auth_request')
sessionSerivce
.logIn(data)
.then((resp) => {
const commitData = {
accessToken: resp.data.access_token,
refreshToken: resp.data.refresh_token,
user: resp.data.user,
}
$cookies.set('accessToken', commitData.accessToken)
$cookies.set('refreshToken', commitData.refreshToken)
$cookies.set('user', JSON.stringify(commitData.user))
axios.defaults.headers.common['Authorization'] =
commitData.accessToken
commit('auth_success', commitData)
resolve(resp)
})
.catch((err) => {
commit('auth_error')
$cookies.remove('accessToken')
$cookies.remove('refreshToken')
$cookies.remove('user')
reject(err)
})
})
},
verifyToken({ commit, state }) {},
register({ commit }, data) {
return new Promise((resolve, reject) => {
commit('auth_request')
sessionSerivce
.register(data)
.then((resp) => {
const commitData = {
accessToken: resp.data.access_token,
refreshToken: resp.data.refresh_token,
user: resp.data.user,
}
$cookies.set('accessToken', commitData.accessToken)
$cookies.set('refreshToken', commitData.refreshToken)
$cookies.set('user', JSON.stringify(commitData.user))
axios.defaults.headers.common['Authorization'] =
commitData.accessToken
commit('auth_success', commitData)
resolve(resp)
})
.catch((err) => {
commit('auth_error')
$cookies.remove('accessToken')
$cookies.remove('refreshToken')
$cookies.remove('user')
reject(err)
})
})
},
logout({ commit }) {
return new Promise((resolve, reject) => {
commit('logout')
$cookies.remove('accessToken')
$cookies.remove('refreshToken')
$cookies.remove('user')
delete axios.defaults.headers.common['Authorization']
resolve()
})
},
},
mutations: {
auth_request(state) {
state.status = 'loading'
},
auth_success(state, commitData) {
state.status = 'success'
state.accessToken = commitData.accessToken
state.refreshToken = commitData.refreshToken
state.user = commitData.user
},
auth_error(state) {
state.status = 'error'
},
refresh_token(state, accessToken) {
state.accessToken = accessToken
},
logout(state) {
state.status = ''
state.accessToken = ''
state.refreshToken = ''
state.user = ''
},
},
getters: {
isLoggedIn: (state) => {
return !!state.accessToken
},
authStatus: (state) => state.status,
},
})
In main.js I use this function to check:
router.beforeEach(async (to, from, next) => {
if (to.matched.some((record) => record.meta.requiresAuth)) {
if (store.getters.isLoggedIn) {
next()
return
}
next('/login')
} else next()
})
The problem is that code above checks only if access token exists in Vuex. I want to verify using api before any route, that requires auth and if it's not successfully I want to refresh It with api using refresh token. If both are unsuccessful(access and refresh tokens are both invalid) user gonna log out.
Example route which requires auth:
path: '/dashboard',
name: 'Dashboard',
component: Dashboard,
meta: {
requiresAuth: true,
},
I've tried code like this:
router.beforeEach(async (to, from, next) => {
if (to.matched.some((record) => record.meta.requiresAuth)) {
if (store.state.accessToken) {
await store.dispatch('verifyToken')
if (store.getters.isLoggedIn) {
next()
return
}
}
next('/login')
} else next()
})
Action in Vuex:
verifyToken({ commit, state }) {
const accessToken = state.accessToken
const refreshToken = state.accessToken
sessionSerivce
.verifyToken(accessToken)
.then((resp) => {})
.catch((err) => {
sessionSerivce
.refreshToken(refreshToken)
.then((resp) => {
console.log('Refreshuje token')
const accessToken = resp.data.access_token
localStorage.setItem('accessToken', accessToken)
axios.defaults.headers.common['Authorization'] = accessToken
commit('refresh_token', accessToken)
})
.catch((err) => {
commit('logout')
localStorage.removeItem('accessToken')
localStorage.removeItem('refreshToken')
delete axios.defaults.headers.common['Authorization']
})
})
},
Note that in code above i used localstorage but i've changed my mind and I'm using cookie, as You can see in previous code.
Unfortunately this code didn't work as expected - if (store.getters.isLoggedIn) { next(); return; } is starting to execute before await store.dispatch('verifyToken') ends, which is bad.
Any ideas?
I'm having a hard time doing this and I'm sure it's simple but I can't get it to work. I have a toggle switch with boolean value that I am successfully making it work from the Vue file but obviously vuex is yelling cause any prop change needs to be mutated in the vuex file. Here is the relevant code:
Vue file
<template>
<workstation
v-for="(workstation, index) in hStation.workstations" :key="index"
:id="workstation.recordId"
:close="workstation.closed"
#toggledState="toggleState(workstation)"
></workstation>
</template>
<script>
methods: {
...mapActions("pod", ["updateWorkstation"]),
toggleState(workstation) {
workstation.closed = !workstation.closed;
this.updateWorkstation({
recordId: workstation.recordId,
closed: workstation.closed
})
.then(response => {
console.log("id: ", workstation.recordId);
console.log("closed: ", workstation.closed);
})
.catch(error => {
console.log("error: ", error);
});
},
},
</script>
The vuex file simplified
import { axiosInstance } from "boot/axios";
export default {
namespaced: true,
state: {
workstation: []
},
getters: {
singleWorkstation: state => {
return state.workstation;
}
},
actions: {
updateWorkstation: ({ commit }, payload) => {
return new Promise((resolve, reject) => {
axiosInstance
.post("Workstation/update", payload)
.then(({ data, status }) => {
if (status === 200) {
resolve(true);
commit("setWorkstation", data.data);
}
})
.catch(({ error }) => {
reject(error);
});
});
}
},
mutations: {
setWorkstation: (state, workstation) => (state.workstation = workstation)
}
};
Error: [vuex] do not mutate vuex store state outside mutation handlers.
API schema
{
"success": true,
"data": [
{
"recordId": 0,
"worksite": 0,
"hStations": [
{
"recordId": 0,
"podId": 0,
"stationOrder": 0,
"workstations": [
{
"recordId": 0,
"name": "string",
"closed": true,
}
]
}
]
}
]
}
How do I fire the change on the close property within the mutation? Thanks in advance
Instead of passing a new object to the action, pass the whole workstation object.
this.updateWorkstation(workstation);
You'll create the posting object, postdata, inside the action, and you'll commit a second mutation for toggling when the promise resolves:
updateWorkstation: ({ commit }, workstation) => {
const postdata = {
recordId: workstation.recordId,
closed: workstation.closed
}
return new Promise((resolve, reject) => {
axiosInstance
.post("Workstation/update", postdata)
.then(({ data, status }) => {
if (status === 200) {
resolve(true);
commit("setWorkstation", data.data);
commit("toggleOldWorkstation", workstation);
}
})
.catch(({ error }) => {
reject(error);
});
});
}
Since the workstation is in the action this way, you're able to call that second mutation to toggle the closed property:
mutations: {
...
toggleOldWorkstation(workstation){
workstation.closed = !workstation.closed;
}
}
I have a CRUD component and the add user (userRegister) is giving me problems. I am able to successfully add a user, however the view doesn't refresh and the new user doesn't show unless I refresh the page.
Here's the method fired on the Submit button in my Vue Component
onSubmit() {
if (this.password === this.confirmpassword) {
this.$store
.dispatch("users/userRegister", {
operatorNumber: this.operatorNumber,
operatorName: this.operatorName,
password: this.password,
roles: this.operatorRole
})
.then(({ status }) => {
UIkit.offcanvas("#newEmployee").hide();
})
.catch(error => {
console.log("Error: ", error);
});
}
this.resetForm();
},
and my store
import { axiosInstance } from "boot/axios";
export default {
namespaced: true,
state: {
operators: []
},
getters: {
operators: state => {
return state.operators;
}
},
actions: {
async userRegister({ commit }, payload) {
return new Promise((resolve, reject) => {
axiosInstance
.post("user/create", payload)
.then(({ data, status }) => {
if (status === 200) {
resolve(true);
commit("addUser", payload);
}
})
.catch(error => {
reject(error);
console.log("Error: ", error);
});
});
},
},
mutations: {
addUser: (state, operators) => state.operators.splice(state.operators.length, 0, operators)
}
};
What am I missing? Thanks
I have the following actions in my Vuex store:
import { HTTP } from '#/services/http'
import router from '#/router'
export const actions = {
loginUser ({ commit, state }, params) {
HTTP.post('v1/login.json', { email: params.email, password: params.password })
.then(response => {
localStorage.setItem('access_token', response.data.token)
router.push({name: 'Hello'})
}).catch(error => {
commit('SET_LOGIN_ERROR', error.response.data.error)
})
},
myAccount ({ commit }) {
HTTP.get('v1/my_account.json').headers({'Authorization': ('Token token=' + localStorage.getItem('access_token'))})
.then(response => {
commit('SET_USER', response.data)
})
}
}
I want to launch myAccount action when loginUser succeeds. How can I do that?
I've tried something like this:
import { HTTP } from '#/services/http'
import router from '#/router'
export const actions = {
loginUser ({ commit, state }, params) {
HTTP.post('v1/login.json', { email: params.email, password: params.password })
.then(response => {
localStorage.setItem('access_token', response.data.token)
router.push({name: 'Hello'})
}).catch(error => {
commit('SET_LOGIN_ERROR', error.response.data.error)
})
},
myAccount ({ dispatch, commit, state }, payload) {
dispatch('loginUser', payload)
.then((res) => {
console.log('dupa')
// Do this when loginUser finished
})
}
}
but this not works...
actions receive the context object, so you can simply either pass the entire object or add dispatch to your destructuring assignment :
const store = new Vuex.Store({
actions: {
foo(context) {
console.log('foo called');
},
bar({dispatch}) {
setTimeout(() => dispatch('foo'), 1000)
}
}
});
Here's the JSFiddle: https://jsfiddle.net/y1527vxh/
Since vue actions can be asynchronous you can add dispatch handler to an action to call another action when it is done;
export const actions = {
loginUser ({ commit, state }, params) {
... // some http request or what would you like to do
},
myAccount ({ dispatch, commit, state }, payload) {
dispatch('loginUser', payload)
.then((res) => {
...
// Do this when loginUser finished
})
},
}
I am doing autentication in my projects like this, i am using axios btw:
loginUser ({ dispatch, commit, state }, payload) {
let loginData = {
username: payload.username,
password: payload.password
}
return axios.post(state.url, loginData)
.then((res) => {
// You can console.log(res.data) to see if your token data is fine
window.localStorage.setItem('AuthTokens', JSON.stringify(res.data))
dispatch('myAccount', { tokens: res.data })
})
.catch((err) => {
// Error handling...
})
},
myAccount ({ commit, state }, { tokens }) {
let headerOptions = {
// Header options with tokens.access_token...
}
return axios.get(state.url, headerOptions)
.then((res) => {
// You have the user data
console.log(res.data)
})
.catch((err) => {
// Error handling...
})
}