We should update the state in mutations and these mutations can be called inside actions. That's OK. But, look at the example below:
actions: {
markMessageAsRead({ state, commit }, payload) {
const messageId = payload.messageId
const messages = state.messages[payload.chatId]
const message = messages[messageId]
message.status = 'read' // <---- THIS ALREADY MUTATES THE STATE!
commit('UPDATE_MESSAGE', { messageId, message }) // <---- BEFORE THIS
},
}
In the above example, before the commit(...) line, the message is getting updated already, because of the message.status = 'read' line.
So, to prevent mutating state in actions, should I always copy an object from the state before changing its props? Is there anything I'm wrong? What's the correct way to get something from state and update it? Hope I explained it well.
If you are updating the payload in action to make your mutations more reusable, then the reusability of a single mutation instead of a more targeted one (SET_STATUS for ex.) does not justify the complexity added by cloning the state before mutating it (look at the code below, it is more clean and simple).
For me, it is a more maintainable and scalable approach to have more targeted mutations, updating only a single prop (when the only purpose is to update a single prop), because this avoid the unnecessary need to always account for every change made to store state as long as the targeted property exists on object.
Thus, in your situation, since your action is called with the only purpose to mark the message as read, I would make a SET_STATUS mutation, and avoid any other manipulations of the state outside mutation.
mutations: {
SET_STATUS(state, {messageId, chatId, status){
state.messages[chatId][messageId].status = status
}
}
actions: {
markMessageAsRead({ state, commit }, payload) {
commit('SET_STATUS', { messageId: payload.messageId, chatId: payload.chatId, status: 'read' })
},
}
Related
I am learning Vuex and my understanding so far has been that mutations should be simple functions that update the state directly using something like state.property = value or state.object = {payload}, e.g.:
SET_USER_DATA (state, userData) {
state.user = userData
}
I am working through a course from Vue Mastery that contains mutations that look like the code below which does not reference the state object within the body of the mutation at all:
CREATE_TASK(state, { tasks, name }) {
tasks.push({ name, id: generateId(), description: "" });
},
UPDATE_TASK(state, { task, field, value }) {
Vue.set(task, field, value);
},
CREATE_TASK creates a new empty task and adds it to the tasks array using tasks.push(), but shouldn't a reference to the state object be required to update the state? E.g. state.tasks.push()? How does simply pushing an item onto a bare array commit the change to the state?
In the second example, they use Vue.set() to update the value of a specific field within a task (e.g. name, description), but again, there is no reference to the state object here.
The best I can figure is that they relying on Vue's native reactivity to automatically update the state when calling Vue.set() or Array.push(). But if that is the case, wouldn't any usage of Vue.set() or Array.push() inside of a component also immediately update the state (violating the rule that state changes should only be handled within a mutation?
I have a button that’s set to update the a store object using Vue.set but the getter for that same piece of data in a different component isn’t reactive until I change the state using a different component method.
The state object in question is set up as a hash that's keyed by UUID's. The object is generated and then added to the state object with Vue.set
The button is set to dispatch an action, which I see it going through immedietely in the devtool, that does this:
mutations: {
COMPLETE_STEP(state, uuid) {
let chat = state.chatStates[uuid];
let step = chat.currentStep;
Vue.set(chat.data[step], "complete", true);
}
},
actions: {
completeStep({ commit }, uuid) {
commit("COMPLETE_STEP", uuid);
}
},
Now, when I want to grab that data, I have a getter that grabs that data. This doesn't run until I do something else that causes a re-render:
getters: {
getChatStepComplete: state => (uuid, step) => {
let chatState = state.chatStates[uuid];
return chatState.data[step].complete;
},
}
I want the getter to show the updated change right away instead of waiting to update on a different re-render. How do I make that happen?
Figured out my issue: I wasn’t creating the data array when I first create and add chat to the state. Once I started initializing it to an empty array, it’s started being reactive.
I have a Vuex store that manages an array (state.all), and I have a button that calls a Vuex action which performs an HTTP call and then appends the the data in the response to state.all by way of a mutation. However, the state never gets updated and the components never update.
In order prove that I was not crazy, I used two alert()s inside of the mutation to make sure I knew where I stood in the code. The alert()s were always fired with proper values.
Here is the truncated Vuex store (this is a module):
const state = {
all: []
}
// actions
const actions = {
...
runner ({ commit, rootState }, { did, tn }) {
HTTP.post(url, payload)
.then(function (response) {
commit('setNewConversations', response.data)
})
})
}
}
const mutations = {
...
setNewConversations(state, new_conv) {
for (let new_c_i in new_conv) {
let new_c = new_conv[new_c_i]
alert(new_c) // I always see this, and it has the correct value
if (!(new_c in state.all)) {
alert('I ALWAYS SEE THIS!') // testing
state.all.push(new_c)
}
}
}
...
}
When I go to test this, I see my two alert()s, the first with the value I expect and the second with "I ALWAYS SEE THIS!" but nothing happens to my v-for component and the state never updates, despite the state.all.push().
What is the next step to troubleshooting this issue? There are no errors in the JS console, and I cannot figure out any reason the state would not be updated.
Thank you!
One possible solution is instead of pushing to the current state value, store the previous value of state.all in a new array and push the new changes to that new array.
Once done, assign that new array to state.all like the following below.
setNewConversations(state, new_conv) {
const prevState = [...state.all];
for (let new_c_i in new_conv) {
let new_c = new_conv[new_c_i]
if (!(new_c in prevState)) {
prevState.push(new_c);
}
}
state.all = prevState;
}
Given that you said that removing the alert makes it work makes me wonder if you are just observing the value in the wrong place. I can't be sure from what you've given.
Remember that Javascript is single-threaded, and your mutation has to complete before any other Vue-injected reactivity code can run.
If you really wanted the value to be shown before the mutation is complete, you could probably call Vue.nextTick(() => alert(...)), but the better answer is to check for the updates somewhere else, such as in a computed that calls the getter for the state.all array.
(By the way, I find that using either console.log(...) or the vue-dev-tools is much faster than alert() for arbitrary debugging.)
I'm trying to set token to my store.token I know this is not a best option without using mutation but I'm doing something like this:
methods : {
molestor(){
const self = this;
this.$store.state.token = "new token";
this.$store.state.cleavage= "yes";
this.$store.commit('settoken', "somethingrandom");
},
}
Then on my store.js:
export const store = new Vuex.Store({
state : {
token : '',
},
mutations : {
settoken(state,token){
console.log(token);
}
}
});
Right now it works fine... it sets up. But when I remove the mutation from store.js or remove the commit on my molester() it wont assign the value to token. Why is this happening?
To set the value of state in store, we have to interact with Vuex api via mutations/commits.
By trying to set the state without a mutation, this goes against the design of Vuex (having a manageable store/state)
Typically trying to set state without mutations (say within an action) will throw an error, but I also believe that by getting the state via ‘$store.state’ will only return the state (and not return the instance of state)
This is done to maintain immutability throughout your application state
If you're expecting to see the change appear in the Vue dev tools you won't see any changes to state unless they occur through a mutation or until another mutation is called.
I'm developing an app using Vuejs and Vuex.
I've got a Vuex module called settings_operations. This module has the following action:
async changePassword ({ commit }, { password, id }) {
commit(CHANGE_PASSWORD_PROCESSING, { id })
const user = auth.currentUser
const [changePasswordError, changePasswordSuccess] = await to(user.updatePassword(password))
if (changePasswordError) {
commit(CHANGE_PASSWORD_ERROR, { id, error: changePasswordError })
} else {
commit(CHANGE_PASSWORD_SUCCESS, changePasswordSuccess)
}
}
Edit: the to() is https://github.com/scopsy/await-to-js
With the following mutations:
[CHANGE_PASSWORD_PROCESSING] (state, { id }) {
state.push({
id,
status: 'processing'
})
},
[CHANGE_PASSWORD_ERROR] (state, { id, error }) {
state.push({
id,
error,
status: 'error'
})
}
And then, in the component I want to use this state slice:
computed: {
...mapState({
settings_operations: state => state.settings_operations
})
},
watch: {
settings_operations: {
handler (newSettings, oldSettings) {
console.log(newSettings)
},
deep: false
}
}
The problem is that when the changePassword action results in an error, the watch doesn't stop in the PROCESSING step, it goes directly to the ERROR moment so the array will be filled with 2 objects. It literally jumps the "processing" watching step.
A funny thing that happens is that if I add a setTimeout just like this:
async changePassword ({ commit }, { password, id }) {
commit(CHANGE_PASSWORD_PROCESSING, { id })
setTimeout(async () => {
const user = auth.currentUser
const [changePasswordError, changePasswordSuccess] = await to(user.updatePassword(password))
if (changePasswordError) {
commit(CHANGE_PASSWORD_ERROR, { id, error: changePasswordError })
} else {
commit(CHANGE_PASSWORD_SUCCESS, changePasswordSuccess)
}
}, 500)
},
It works! The watch stops two times: the first tick displaying the array with the processing object and the second tick displaying the array with 2 objects; the processing one and the error one.
What am I missing here?
Edit:
I reproduced the problem here: https://codesandbox.io/s/m40jz26npp
This was the response given in Vue forums by a core team member:
Watchers are not run every time the underlying data changes. They are only run once on the next Tick if their watched data changed at least once.
your rejected Promise in the try block is only a microtask, it doesn’t
push execution to the next call stack (on which the watchers would be
run), so the error handling happens before the watchers are run.
additionally, when you mutat an object or array, the newValue and
oldValue in a deep watcher will be the same. See the docs:
Note: when mutating (rather than replacing) an Object or an Array, the old value will be the same as new value because they reference the
same Object/Array. Vue doesn’t keep a copy of the pre-mutate value.
and as a final sidenote, I’ve never seen anyone use an aray as the
root state of a module, I have no idea if that will work for vuex in
all possible circumstances. I certainly would not recommend doing
this.
Edit with a better and more complete answer from the same member:
Why watchers are asynchronous at all? Because in the vast majority of
use cases, watchers only need to react to the last synchrnous change
that was done. In most cases (in the context of a component), it would
be couterproductive to to react to every change since you would
re-trigger the same behaviour mutliple times even though in the end,
only the last state is the important one.
In other words: Running a watcher on each change by default would
probably lead to apps that burn a lot of CPU cycles doing useless
work. So watchers are implemented with an asynchronous queue that is
only flushed on nexTick. And we don’t allow duplicate watchers then
because the older instance of a watcher would apply to data that
doesn’t “exist” anymore in that state once the queue is flushed.
An important note would be that this only applies to synchronous
changes or those done within a microtask, i.e. in an immediatly
resolving or failing promise - it would, for example, not happen with
an ajax request.
Why are they implemented in a way that they are still not run after a
microtask (i.e. an immediatly resolved promise? That’s a bit more
coplicated to explain and requires a bit of history.
Originally, in Vue 2.0, Vue.nextTick was implemented as a microtask
itself, and the watcher queue is flushed on nextTick. That meant that
back then, a watcher watching a piece of data that was changed two
times, with a microtask (like a promise) in between, would indeed run
two times.
Then, around 2.4 I think, we discovered a problem with this
implementation and switched Vue.nextTick to a macroTask instead. under
this behaviour, both data chhanged would happen on the current call
stack’s microtaks queue, while the watcher queue would be flushed at
th beginning of the next call stack, wich means it will only run once.
We found a couple of new problems with this implementation that are
much more common than the original issue with microtasks, so we will
likely switch back to the microtask implementation in 2.6. Ugly, but
necessary.
So, this should do the trick for now:
await Vue.nextTick();