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.)
Related
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' })
},
}
Say I have an action like this:
async getYoutubeReport({ commit }, payload) {
return await this.$axios
.get(
`youtube/reports/${payload.date}/${payload.coin}`,
).then((res) => {
let today = utils.yearMonthDay(new Date())
let report = coinData.data.Items.find(Report => utils.yearMonthDay(new Date(Report.createdAt)) == today)
commit('SET_YOUTUBE_REPORT', report)
return report
})
}
}
Does this make any sense at all?
After all, the whole purpose of the Vuex Store is to horizontally make variables accesible across the app which can easily be done with reactive getters.
If the answer is "Yes, it is totally Fine", under which circumstances would make sense?
IMO, the only circumstance where I see it useful and where I do it is for showing a message response from the API in your Vue component.
If you need to access your state data you should use reactive getters.
Otherwise, there is no reason to maintain a vuex store and use data returned from your API.
I am trying to show some dynamic content in my component but somehow useEffect causes a infinite loop.
What can be the problem?
useEffect(() => {
retrieveLocalData('following').then((contacts) => {
setLocalData(JSON.parse(contacts));
});
}, [getLocalData]);
async function retrieveLocalData(key) {
try {
return await AsyncStorage.getItem(key);
} catch (error) {
console.log(error);
}
}
console.log('test'); // infinite
Code: https://codepen.io/eneskul/pen/OJWEgmw
Updated Answer
The infinite loop is a result of the useEffect hook updating the same value that is triggering the hook to run in the first place.
Here's a simple example to illustrate the problem:
const [value, setValue] = useState({ foo: 'bar' });
useEffect(() => {
Promise.resolve('{"foo":"bar"}').then((result) => {
const newValue = JSON.parse(result);
// `newValue` is a new object, even if its content is identical to `value`.
setValue(newValue);
});
}, [value]);
In this example, when value is set, it causes the useEffect hook to execute, which will asynchronously update value with a new object, which will cause the useEffect hook to execute again, and so on. Even though the contents of the objects are identical, the JSON.parse call creates a new object with a new reference.
You can prevent the infinite loop by doing a deep equality check of the two objects before updating the state. Using something like Lodash's isEqual function makes this pretty easy.
useEffect(() => {
Promise.resolve('{"foo":"bar"}').then((result) => {
setValue((prev) => {
const newValue = JSON.parse(result);
// Do a deep comparison and only update state with new object if content is different.
return isEqual(prev, newValue) ? prev : newValue;
});
});
}, [value]);
In this example, the reference to value will only change if the contents of the objects are different.
However, this only explains what the problem is. I'm not sure what the right solution is for your problem, since it's not clear why the component only needs to load data from local storage into state when the state changes, but the state is only updated when it loads from local storage. There seems to be a "chicken or the egg" problem here. It feels like there should be something else that should trigger loading data from local storage into state, other than the data that was just loaded from local storage into state.
Previous Answer
The likely culprit here is getLocalData in the dependency list of the useEffect hook. If that is not a stable reference (i.e. the reference changes on each render), then it will cause the useEffect hook to execute, which will then trigger a state update, which will trigger a render, which will cause useEffect to execute again, which starts the whole thing over again.
In the sample code, it's not clear where getLocalData comes from. Wherever it comes from, you might consider wrapping it with the useCallback hook to create a stable reference. If it's just a typo and meant to be retrieveLocalData, then that is definitely the issue. Because retrieveLocalData is declared inside the component's render function, it will create a new instance of the function (with a new reference) on each render.
I would just move it inside the useEffect hook and eliminate the dependencies.
useEffect(() => {
AsyncStorage.getItem('following')
.then((contacts) => {
setLocalData(JSON.parse(contacts));
})
.catch((error) => {
console.log(error);
});
}, []);
We are trying to detect whether a person is logged in or not using the vuex store state: loggedIn. When I call the API service from the action it calls the mutation after successful login and changes the data in the state:
loginSuccess(state, accessToken) {
state.accessToken = accessToken;
state.authenticating = false;
state.loggedIn = true;
console.log(state.loggedIn);
}
The console.log() shows the value, so the mutation is working.
In my other component, I use a computed property to watch for changes in the store using ...mapState() and bound the property in the template view:
computed: {
...mapState('authStore',['loggedIn' ]);
}
But the view never gets updated based on the computed property. I checked using the Vue dev tools in the console. It shows the state changes.
I have initialized the state.
export const states = {
loggedIn: false
};
I have tried to call the state directly.
this.$store.state.authStore.loggedIn;
I have tried different approaches.
...mapState('authStore', { logging:'loggedIn' });
//or
...mapState('authStore',['loggedIn' ]);
also, tried watch: {} hook but not working.
Interestingly though, the state's getter always shows undefined, but the state property changes in the dev tools.
Cannot figure out what is wrong or how to move further.
here is the screenshot of devtools state after successful login:
This catches my eye:
export const states = {
loggedIn: false
};
My suspicion is that you're then trying to use it something like this:
const store = {
states,
mutations,
actions,
getters
}
This won't work because it needs to be called state and not states. The result will be that loggedIn is unreactive and has an initial value of undefined. Any computed properties, including the store's getter, will not be refreshed when the value changes.
Whether my theory is right or not, I suggest adding console.log(state.loggedIn); to the beginning of loginSucess to confirm the state prior to the mutation.
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();