Vuex state module structure - vue.js

I adopted the file structure on vuex with modules. Originally I just had everything in one store file (I don't know what I was thinking). Now that I refactored the code to a better more maintainable structure I am having issues with how to mimic the state that I had before.
My previous state for the user field was just a user object like this:
user: {...}
Now that I used this format
const state = {
}
const mutations = {
fetchUser(state,user){
console.log(user)
state = user;
}
};
const actions = {
currentUser: ({commit}) => {
axios.get('/user').then(response => {
if(response.status == 200){
commit('fetchUser', response.data.data);
}
}).catch(response => {
});
}
}
My state translates to :
user:{}
with an empty object. Shouldn't this assign the user into that user state object or am I missing something.

From docs:
Inside a module's mutations and getters, the first argument received will be the module's local state.
So your mutation should access the module object:
const mutations = {
setUser(state, user){
state.user = user; // Assuming you have a user module **
}
};
** Assuming you have a user module this way:
const store = new Vuex.Store({
modules: {
user: moduleUser,
// more modules ...
}
})
Mutations should only modified the state. You should change your logic to fetch user data from an action only and not from your mutation itself, for example:
const actions = {
currentUser: ({commit}) => {
axios.get('/user').then(response => {
if(response.status == 200){
var response = response.data.data;
commit('setUser', response);
}
}).catch(response => {
});
}
}

Related

Vue3 / Vuex State is empty when dispatching action inside of lifecycle hook inside of test

We're using the composition API with Vue 3.
We have a Vuex store that, amongst other things, stores the currentUser.
The currentUser can be null or an object { id: 'user-uuid' }.
We're using Vue Test Utils, and they've documented how to use the store inside of tests when using the Composition API. We're using the store without an injection key, and so they document to do it like so:
import { createStore } from 'vuex'
const store = createStore({
// ...
})
const wrapper = mount(App, {
global: {
provide: {
store: store
},
},
})
I have a component and before it is mounted I want to check if I have an access token and no user currently in the store.
If this is the case, we want to fetch the current user (which is an action).
This looks like so:
setup() {
const tokenService = new TokenService();
const store = useStore();
onBeforeMount(async () => {
if (tokenService.getAccessToken() && !store.state.currentUser) {
await store.dispatch(FETCH_CURRENT_USER);
console.log('User: ', store.state.currentUser);
}
});
}
I then have a test for this that looks like this:
it('should fetch the current user if there is an access token and user does not exist', async () => {
localStorage.setItem('access_token', 'le-token');
await shallowMount(App, {
global: {
provide: {
store
}
}
});
expect(store.state.currentUser).toStrictEqual({ id: 'user-uuid' });
});
The test fails, but interestingly, the console log of the currentUser in state is not empty:
console.log src/App.vue:27
User: { id: 'user-uuid' }
Error: expect(received).toStrictEqual(expected) // deep equality
Expected: {"id": "user-uuid"} Received: null
Despite the test failure, this works in the browser correctly.
Interestingly, if I extract the logic to a method on the component and then call that from within the onBeforeMount hook and use the method in my test, it passes:
setup() {
const tokenService = new TokenService();
const store = useStore();
const rehydrateUserState = async () => {
if (tokenService.getAccessToken() && !store.state.currentUser) {
await store.dispatch(FETCH_CURRENT_USER);
console.log('User: ', store.state.currentUser);
}
};
onBeforeMount(async () => {
await rehydrateUserState();
});
return {
rehydrateUserState
};
}
it('should fetch the current user if there is an access token and user does not exist', async () => {
localStorage.setItem('access_token', 'le-token');
await cmp.vm.rehydrateUserState();
expect(store.state.currentUser).toStrictEqual({ id: 'user-uuid' });
});
Any ideas on why this works when extracted to a method but not when inlined into the onBeforeMount hook?

strange console.log output with vuex

i have some simple vuex store with
const state = {
todos : []
}
const getters = {
allTodos: (state) => state.todos
}
const actions = {
async fetchTodos({ commit }) {
console.log(this.state.todos)
if(state.todos.length == 0) {
const response = await axios.get('https://jsonplaceholder.typicode.com/todos?_limit=5')
commit('setTodos', response.data)
}
}
}
const mutations = {
setTodos(state, todos) {
state.todos = todos
}
}
why does console.log in fetchTodos action output populated todos before it was populated with axios.get and setTodos mutation?
when i write
const actions = {
fetchTodos({ commit }) {
console.log(this.state.todos)
setTimeout(async () => {
if(state.todos.length == 0) {
const response = await axios.get('https://jsonplaceholder.typicode.com/todos?_limit=5')
commit('setTodos', response.data)
}
}, 10000)
}
}
output is normal with empty todos in state
That's because you will see a little blue triangle right next to the console log. I don't know the technical term for it but what happens is that the browser will update that variable with the current value because it is a reactive variable and since it is a reference being pointed to a location in memory, it will update.
If you truly wish to see the value and prove what was described above, you can write:
console.log(JSON.parse(JSON.stringify(this.state.todos)));

Can I dispatch vuex action inside getter function?

I want to dispatch action inside getter function.
1. Is it possible and right.
2. If yes how can I do it?
I guess it will be something like this dispatch('GET_BOOKS');
const getters = {
getAllBooksDispatch: (state, getters, dispatch) => {
if (state.books === null) {
dispatch('GET_BOOKS');
}
return state.books
},
};
But it does not work.
So my store file looks like this.
const initialState = {
books: null
};
const getters = {
getAllBooksDispatch: (state, getters, dispatch) => {
if (state.books === null) {
dispatch('GET_BOOKS');
}
return state.books
},
};
const mutations = {
SET_BOOKS: (state,{data}) => {
console.log('SET_BOOKS mutations')
state.books = data;
},
};
const actions = {
GET_BOOKS: async ({ commit }) => {
let token = users.getters.getToken;
let query = new Promise((resolve, reject) => {
axios.get(config.api + 'books', {token}).then(({data}) => {
if (data) {
commit('SET_BOOKS', {data: data})
resolve()
} else {
reject(data.message);
}
}).catch(() => {
reject('Error sending request to server!');
})
})
},
};
No, you can't. At least not the way you want to. The third argument in a getter is the rootState object when using modules, not dispatch. Even if you find a way to dispatch an action inside a getter it won't work the way you expect. Getters must be synchronous, but actions can be (and in this example are) asynchronous. In your example, GET_BOOKS would be dispatched but the getter would still return state.books as null.
I'd recommend handling this sort of lazy-loading outside of the Vuex store.

Vuex - Cloned state into data object property, can't delete array item

I've cloned my state from Vuex to an array in my component, data(). My problem is when I'm trying to remove the first item in the array from my clone with shift()and also add it back with unshift() I get this error msg:
[vuex] Do not mutate vuex store state outside mutation handlers.
How can I delete something in my cloned state that's not effects the actually state itself?
Saving/cloning
beforeMount () {
this.traningArea = this.stateExercises
},
computed: {
...mapState({
userStore: state => state.userStore,
tsStore: state => state.trainingSchemeStore
}),
stateExercises () {
return this.tsStore.schemeExercises
}
}
Trying to run shift() on click and unshift() if user click again
this.traningArea[0].shift()
this.traningArea[0].unshift(obj)
And it's here I've got this error.
STATE
const state = {
trainings: []
}
const actions = {
getTrainingExercise: ({commit}, ids) => {
var payload = {
'trainings_id': ids
}
return Vue.http.post(getTrainingsById, payload,
{headers: getHeader()})
.then(response => {
if (response.status === 200) {
commit('SET_TERL', response.body.data)
}
})
},
const mutations = {
SET_TERL(state, trainings) {
state.trainings.push(trainings)
}
}
i hope that i don't misunderstand you, so i think that the solution would be like this :
const mutations = {
SET_TERL(state, trainings) {
state.trainings.push(trainings)
},
SHIFT(state, index) {
state.trainings[index].shift()
},
UNSHIFT(state, index,obj) {
state.trainings[index].unshift(obj)
}
}
and when you call the method :
this.tsStore.commit("SHIFT",0);
or
this.tsStore.commit("UNSHIFT",0,obj);

"Simulate" mutations in vuex

import { remoteSettings } from 'somewhere';
const store = {
state: {
view: {
foo: true
}
},
mutations: {
toggleFoo(state) {
state.view.foo = !state.view.foo;
}
},
actions: {
async toggleFoo({ state, commit }) {
commit('toggleFoo');
await remoteSettings.save(state);
}
}
};
Say I have a simple store like this. toggleFoo action applies the mutation, then saves the new state by making an async call. However, if remoteSettings.save() call fails, local setting I have in the store and remote settings are out of sync. What I really want to achieve in this action is something like this:
async toggleFoo({ state, commit }) {
const newState = simulateCommit('toggleFoo');
await remoteSettings.save(newState);
commit('toggleFoo');
}
I'd like to get the new state without actually committing it. If remote call succeeds, then I'll actually update the store. If not, it's going to stay as it is.
What's the best way to achieve this (without actually duplicating the logic in the mutation function)? Maybe "undo"? I'm not sure.
One way of doing this would be: (credit to #Bert for correcting mistakes)
Store the old state using const oldState = state; before committing the mutation.
Wrap the async call in a try-catch block.
If the remoteSettings fails it will pass the execution to catch block.
In the catch block commit a mutation to reset the state.
Example:
const store = {
state: {
view: {
foo: true
}
},
mutations: {
toggleFoo(state) {
state.view.foo = !state.view.foo;
},
resetState(state, oldState){
//state = oldState; do not do this
//use store's instance method replaceState method to replace rootState
//see : https://vuex.vuejs.org/en/api.html
this.replaceState(oldState)
}
},
actions: {
async toggleFoo({ state, commit }) {
const oldState = JSON.parse(JSON.stringify(state)); //making a deep copy of the state object
commit('toggleFoo');
try {
await remoteSettings.save(newState);
//commit('toggleFoo'); no need to call this since mutation already commited
} catch(err) {
//remoteSettings failed
commit('resetState', oldState)
}
}
}
};
Borrowing code from #VamsiKrishna (thank you), I suggest an alternative. In my opinion, you want to send the changes to the server, and update the local state on success. Here is a working example.
To prevent duplicating logic, abstract the change into a function.
console.clear()
const remoteSettings = {
save(state){
return new Promise((resolve, reject) => setTimeout(() => reject("Server rejected the update!"), 1000))
}
}
function updateFoo(state){
state.view.foo = !state.view.foo
}
const store = new Vuex.Store({
state: {
view: {
foo: true
}
},
mutations: {
toggleFoo(state) {
updateFoo(state)
},
},
actions: {
async toggleFoo({ state, commit }) {
// Make a copy of the state. This simply uses JSON stringify/parse
// but any technique/library for deep copy will do. Honestly, I don't
// think you would be sending the *entire* state, but rather only
// what you want to change
const oldState = JSON.parse(JSON.stringify(state))
// update the copy
updateFoo(oldState)
try {
// Attempt to save
await remoteSettings.save(oldState);
// Only commit locally if the server OKs the change
commit('toggleFoo');
} catch(err) {
// Otherwise, notify the user the change wasn't allowed
console.log("Notify the user in some way that the update failed", err)
}
}
}
})
new Vue({
el: "#app",
store,
computed:{
foo(){
return this.$store.state.view.foo
}
},
mounted(){
setTimeout(() => this.$store.dispatch("toggleFoo"), 1000)
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vuex/3.0.1/vuex.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.9/vue.js"></script>
<div id="app">
<h4>This value never changes, because the server rejects the change</h4>
{{foo}}
</div>