Vuex action with ajax not updated in computed - javascript

I use Vue with Vuex. In one case I use Ajax to get a presentation value. Somewhere on the way, probably in computed it's no longer reactive.
In my component:
props: [ 'x', 'y' ],
template: `
<div class="presentation">
{{ presentation }}
</div>
`,
computed: {
presentation() {
return this.$store.getters.presentation({ x: this.x, y: this.y });
}
}
Vuex store:
const store = new Vuex.Store({
state: {
table: {
data: []
}
},
...
Vuex actions:
I call an url with ajax and return a promise. I also commit a mutation.
actions: {
save: (context) => {
let uri = 'some/uri';
let params = {};
let value = 'A value';
return axios
.post(uri, params)
.then((response) => {
context.commit('setPresentation', value);
})
.catch((error) => {
console.log('error');
})
.finally(() => {});
},
},
Vuex mutations:
mutations: {
setPresentation: (state, value) => {
let pos = state.table.pos;
state.table.data[pos.y][pos.x].presentation = value;
},
}
Vuex getters:
getters: {
presentation: (state) => (pos) => {
return state.table.data[pos.y][pos.x].presentation;
}
},
I've already make sure of the following:
I set up the table.data state to a default value to make it reactive
Using a getter to get the data
Using an action for the ajax call
Call a mutation with a commit in the action
Notes:
The ajax call needs to be in an action and not in created, because I'm going to use presentation from more than one component.
I prefer a solution which does not need external Vue plugins.
Question(s)
What did I miss?
How can I solve it in the best way?

You need to use Vue.set instead of state.table.data[pos.y][pos.x].presentation = value;
See https://v2.vuejs.org/v2/guide/list.html#Caveats for details
Try to update your mutation with the following code:
if (!state.table.data[pos.y]) {
Vue.set(state.table.data, pos.y, [])
}
Vue.set(state.table.data[pos.y], pos.x, { presentation: value })
A word from me, the OP (Original poster):
Why it failed the first times was that I only set the last part { presentation: value } with Vue.set as I already has pos.y and pos.x set in another ajax call.
For Vue to be fully aware if the change I needed to set everything that has not already been set in state, with Vue.set. So I needed use Vue.set to set pos.y and pos.x as well.
Also see another excellent answer below.
Vue cannot detect changes to an array when you directly set an item with the index

Your mutation is OK; there's no issues there. Vue will detect the assignment to the presentation property of the object just fine as long as the object is being observed by Vue.
In most cases Vue will automatically observe objects, but there are some quirks (especially with arrays) that you need to be aware of.
Vue cannot detect changes to an array when you directly set an item with the index (docs).
I assume you are populating your arrays in the following manner:
for (let y = 0; y < Y_MAX; y++) {
// This is bad! Vue cannot detect this change
state.table.data[y] = []
for (let x = 0; x < X_MAX; x++) {
// Same as above, Vue cannot detect this change. As a consequence,
// the object you are assigning to the array will not be observed
// by Vue! So if you were to mutate the presentation property
// of this object, Vue won't know about it.
state.table.data[y][x] = { presentation: '...' }
}
}
So to fix your problem you just need to make sure you are not mutating arrays in the following way:
array[index] = whatever
You need to do this instead:
Vue.set(array, index, whatever)
Alternatively, you can build the array first and then assign it to state.table.data last; Vue will detect the assignment and then recursively observe the array and everything contained within it.
const data = []
for (let y = 0; y < Y_MAX; y++) {
data[y] = []
for (let x = 0; x < X_MAX; x++) {
data[y][x] = { presentation: '...' }
}
}
// After this assignment, data (and all objects contained within it)
// will be observable
state.table.data = data

It looks like your props is an array. Are you sure this.x and this.y return the correct values inside your presentation method?

Related

Why doesn't custom hook return updated data?

There is a custom useMarkers hook for converting markers into clusters on the map.
It takes needed props,but the essential one are options prop.
const { markersArray, supercluster } = useMarkers({
points: transformedMarkers,
bounds,
zoom,
options: defaultOptions2, // <=== HERE IS WHAT WE NEED
});
And here is it from the inside:
const initialMarkersData = {
markersArray: [],
supercluster: []
};
const useMarkers = (initialData = {}) => {
const [markersData, setMarkersData] = useState(initialMarkersData);
const { clusters, supercluster } = useSupercluster(initialData);
useEffect(() => {
if (initialData.zoom <= 17) {
// console.log("SET NON-CLUSTERS");
setMarkersData({
...markersData,
markersArray: initialData.points,
});
} else {
// console.log("SET CLUSTERS");
setMarkersData({
markersArray: clusters,
supercluster,
});
}
}, [initialData.zoom, clusters, supercluster]);
return markersData;
};
Inside useMarkers I`m passing props to another hook, useSupercluster (comes from separate npm library), and depending on options prop it should return an updated clusters array, and depending on the zoom level an additional useEffect should change the markersData, which is then returns as a result.
And the problem is that when I add a condition for passing different options, the new array is not returned.
For example, if I pass options like this, it works:
options: defaultOptions2,
But if I add condition instead of just single defaultOptions2
options: zoom < 17 ? defaultOptions : defaultOptions2
It doesn't work. If I console.log initialData inside useMarkers hook, I will see new options, so the condition works correct, but although this hook is re-renders, it looks like useSupercluster() doesn`t reinitialize again because it doesn't return modified array according to new options. What could be the reason?
UPDATE*: I tried to investigate this behavior with debugger and everything looks correct. The supercluster property, which is destructured from useSupercluster hook, has to have the this options in it, and although new options successfully passed to useSupercluster within initialData, supercluster property got the empty object at the end;

Redux how to update nested state object immutably without useless shallow copies?

Im using NGRX store on angular project.
This is the state type:
export interface TagsMap {
[key:number]: { // lets call this key - user id.
[key: number]: number // and this key - tag id.
}
}
So for example:
{5: {1: 1, 2: 2}, 6: {3: 3}}
User 5 has tags: 1,2, and user 6 has tag 3.
I have above 200k keys in this state, and would like to make the updates as efficient as possible.
The operation is to add a tag to all the users in the state.
I tried the best practice approach like so :
const addTagToAllUsers = (state, tagIdToAdd) => {
const userIds = Object.keys(state.userTags);
return userIds.reduce((acc, contactId, index) => {
return {
...acc,
[contactId]: {
...acc[contactId],
[tagIdToAdd]: tagIdToAdd
}
};
}, state.userTags);
};
But unfortunately this makes the browser crush when there are over 200k users and around 5 tags each.
I managed to make it work with this:
const addTagToAllUsers = (state, tagIdToAdd) => {
const stateUserTagsShallowCopy = {...state.userTags};
const userIds = Object.keys(stateUserTags);
for (let i = 0; i <= userIds.length - 1; i++) {
const currUserId = userIds[i];
stateUserTagsShallowCopy[currUserId] = {
...stateUserTagsShallowCopy[currUserId],
[tagIdToAdd]: tagIdToAdd
};
}
return stateUserTagsShallowCopy;
};
And the components are updated from the store nicely without any bugs as far as I checked.
But as written here: Redux website mentions :
The key to updating nested data is that every level of nesting must be copied and updated appropriately
Therefore I wonder if my solution is bad.
Questions:
I Believe I'm still shallow coping all levels in state, am i wrong ?
Is it a bad solution? if so what bugs may it produce that I might be missing ?
Why is it required to update sub nested level state in an immutable manner, if the store selector will still fire because the parent reference indeed changed. (Since it works with shallow checks on the top level.)
What is the best efficient solution ?
Regarding question 3, here is an example of the selector code :
import {createFeatureSelector, createSelector, select} from '#ngrx/store';//ngrx version 10.0.0
//The reducer
const reducers = {
userTags: (state, action) => {
//the reducer function..
}
}
//then on app module:
StoreModule.forRoot(reducers)
//The selector :
const stateToUserTags = createSelector(
createFeatureSelector('userTags'),
(userTags) => {
//this will execute whenever userTags state is updated, as long as it passes the shallow check comparison.
//hence the question why is it required to return a new reference to every nested level object of the state...
return userTags;
}
)
//this.store is NGRX: Store<State>
const tags$: Observable<any> = this.store.pipe(select(stateToUser))
//then in component I use it something like this:
<tagsList tags=[tags$ | async]>
</tagsList>
Your solution is perfectly fine.
The rule of thumb is that you cannot mutate an object/array stored in state.
In Your example, the only thing that You are mutating is the stateUserTagsShallowCopy object (it is not stored inside state since it is a shallow copy of state.userTags).
Sidenote: It is better to use for of here since you don't need to access the index
const addTagToAllUsers = (state, tagIdToAdd) => {
const stateUserTagsShallowCopy = {...state.userTags};
const userIds = Object.keys(stateUserTags);
for (let currUserId of userIds) {
stateUserTagsShallowCopy[currUserId] = {
...stateUserTagsShallowCopy[currUserId],
[tagIdToAdd]: tagIdToAdd
};
}
return stateUserTagsShallowCopy;
};
If you decide to use immer this will look like this
import produce from "immer";
const addTagToAllUsers = (state, tagIdToAdd) => {
const updatedStateUserTags = produce(state.userTags, draft => {
for (let draftTags of Object.values(draft)) {
tags[tagIdToAdd] = tagIdToAdd
}
})
return updatedStateUserTags
});
(this comes usually with performance cost). With immer you can sacrifice performance to gain readability
ad 3.
Why is it required to update sub nested level state in an immutable manner, if the store selector will still fire because the parent reference indeed changed. (Since it works with shallow checks on the top level.)
Every store change selectors recompute to see if the dependent component should re-render.
imagine that instead of immutable update of the user tags we decided to mutate tags inside user (state.userTags is a new object reference but we mutate (reuse) old entries objects state.userTags[userId])
const addTagToAllUsers = (state, tagIdToAdd) => {
const stateUserTagsShallowCopy = {...state.userTags};
const userIds = Object.keys(stateUserTags);
for (let currUserId of userIds) {
stateUserTagsShallowCopy[currUserId][tagIdToAdd] = tagIdToAdd;
}
return stateUserTagsShallowCopy;
};
In your case, you have a selector that takes out state.userTags.
It means that every time a state update happens nrgx will compare the previous result of the selector and the current one (prevUserTags === currUserTags by reference). In our case, we change state.userTags so the component that uses this selector will be refreshed with new userTags.
But imagine other selectors that instead of all userTags will select only one user tags. In our imaginary situation, we mutate directly userTags[someUserId] so the reference remains the same each time. The negative effect here is that subscribing component will be not refreshed (will not see an update after a tag is added).

Why immer.js doesn't allow setting dynamic properties on draft?

//I want my action to dispatch payload like
// {type:'update',payload:{'current.contact.mobile':'XXXXXXXXX'}}
//In reducer dynamically select the segment of state update needs to be applied to
//Below code doesn't work as expected though, draft always remains at same level
draft = dA.key.split('.').reduce((draft, k) => {
return draft[k]
}, draft);
//Or an ideal syntax may look like below line
draft['current.contact.mobile'] = dA.value;
//Code that works
draft['current']['contact']['mobile'] = dA.value;
I want my action to dispatch payload like
{type:'update',payload:{'current.contact.mobile':'XXXXXXXXX'}}
And in reducer dynamically select the segment of state that needs to be updated.
Is there something fundamentally wrong in doing this, I believe this could make life easier. Is there something that can done to achieve this ?
In your case, this code returns a primitive value like a string or number which is immutable.
draft = dA.key.split('.').reduce((draft, k) => {
return draft[k]
}, draft);
"Immer" is using Proxy to implement all this magic. The proxy could work only on objects for example Object, Array, Function etc.
so to fix your problem you can use code like this
import produce from "immer";
describe("Why immer.js doesn't allow setting dynamic properties on draft?", function() {
it("should allow set dynamic properties", function() {
const path = "foo.bar.zoo";
const state = { foo: { bar: { zoo: 1 } } };
const nextState = produce(state, draft => {
const vector = path.split(".");
const propName = vector.pop();
if (propName) {
draft = vector.reduce((it, prop) => it[prop], draft);
draft[propName] += 1;
}
});
expect(nextState.foo.bar.zoo).toEqual(state.foo.bar.zoo + 1);
});
});
In the code above, we get destination object and update the property of this object.
Some note about string and number.
Javascript has constructors for string and number which return objects not primitive values. But this is a very rare case when someone uses it explicitly.
Usually, we deal with it implicitly when writing something like this dA.key.split('.'). In this case, the interpreter would create a string object and call method "split" on it. Usually, this behavior is referred to as "Boxing"

How to modify a 'value' prop in VueJS before `$emit('input')` finishes updating it

I have a question about creating VueJS components that are usable with v-model which utilise underlying value prop and $emit('input', newVal).
props: {
value: Array
},
methods: {
moveIdToIndex (id, newIndex) {
const newArrayHead = this.value
.slice(0, newIndex)
.filter(_id => _id !== id)
const newArrayTail = this.value
.slice(newIndex)
.filter(_id => _id !== id)
const newArray = [...newArrayHead, id, ...newArrayTail]
return this.updateArray(newArray)
},
updateArray (newArray) {
this.$emit('input', newArray)
}
}
In the above code sample, if I do two modifications in quick succession, they will both be executed onto the "old array" (the non-modified value prop).
moveIdToIndex('a', 4)
moveIdToIndex('b', 2)
In other words, I need to wait for the value to be updated via the $emit('input') in order for the second call to moveIdToIndex to use that already modified array.
Bad solution 1
One workaround is changing updateArray to:
updateArray (newArray) {
return new Promise((resolve, reject) => {
this.$emit('input', newArray)
this.$nextTick(resolve)
})
}
and execute like so:
await moveIdToIndex('a', 4)
moveIdToIndex('b', 2)
But I do not want to do this, because I need to execute this action on an array of Ids and move them all to different locations at the same time. And awaiting would greatly reduce performance.
Bad solution 2
A much better solution I found is to just do this:
updateArray (newArray) {
this.value = newArray
this.$emit('input', newArray)
}
Then I don't need to wait for the $emit to complete at all.
However, in this case, VueJS gives a console error:
Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "value"
Does anyone have any better solution?
OK. These are your options as far as I understand your use case and application.
First of all, don't mutate the props directly save the props internally and then modify that value.
props: {
value: Array
},
data() {
return {
val: this.value
}
}
If the next modification to the array is dependent on the previous modification to the array you can't perform them simultaneously. But you need it to happen fairly quickly ( i will assume that you want the user to feel that it's happening quickly ). What you can do is perform the modification on the val inside the component and not make it dependent on the prop. The val variable is only initialized when the component is mounted. This way you can modify the data instantly in the UI and let the database update in the background.
In other words, your complete solution would look like this:
props: {
value: Array
},
data () {
return {val: this.value}
},
methods: {
moveIdToIndex (id, newIndex) {
const newArrayHead = this.val
.slice(0, newIndex)
.filter(_id => _id !== id)
const newArrayTail = this.val
.slice(newIndex)
.filter(_id => _id !== id)
const newArray = [...newArrayHead, id, ...newArrayTail]
return this.updateArray(newArray)
},
updateArray (newArray) {
this.val = newArray
this.$emit('input', newArray)
}
}
This solution fixes your problem and allows you to execute moveIdToIndex in quick succession without having to await anything.
Now if the array is used in many places in the application next best thing would be to move it to a store and use it as a single point of truth and update that and use that to update your component. Your state will update quickly not simultaneously and then defer the update to the database for a suitable time.
Emit a message to the parent to change the prop.
Put a watcher on the prop (in the child) and put your code to use the new value there.
This keeps the child from mutating the data it does not own, and allows it to avoid using nextTick. Now your code is asynchronous and reactive, without relying on non-deterministic delays.
How about making copy of the value ?
moveIdToIndex (id, newIndex) {
const valueCopy = [...this.value]
const newArrayHead = this.valueCopy
.slice(0, newIndex)
.filter(_id => _id !== id)
const newArrayTail = this.valueCopy
.slice(newIndex)
.filter(_id => _id !== id)
const newArray = [...newArrayHead, id, ...newArrayTail]
return this.updateArray(newArray)

VueJS $set with variable keypath

I'm currently working on a simple filemanager component which I trigger from parent component. After selecting media in the filemanager I $dispatch a simple data object with 2 keys: element & media. I use element to keep track where I want the media to be appended to my current data object and media has the media information (id, type, name and so on). This setup gives me some trouble when I want to $set the media data to variables within my data object. The variables are locales, so: nl-NL, de-NL and so on.
setMediaForPage : function(data){
if(!this.page.media[this.selectedLanguage]['id'])
{
// set for all locales
var obj = this;
this.application.locales.forEach(function(element, index, array) {
obj.$set(obj.page.media[element.locale], data.media);
})
}
else
{
// set for 1 locale
this.$set(this.page.media[this.selectedLanguage], data.media);
}
}
What happens when I run this code is that the data object shows up properly in Vue Devtools data object, but the media does not show up in the template. When I switch the language (by changing the this.selectedLanguage value), the media does show up.
I think this has to do with the variables in the object keypath, but I'm not 100% sure about that. Any thoughts on how to improve this code so I can show the selected media in the parent component without having to change the this.selectedLanguagevalue?
I don't know your data structure exactly, but you can certainly use variables as the the keypath in vue, however remember that the keyPath should be a string, not an object.
If your variable that you want to use in the keypath is part of the vue, you'd do it like this:
obj.$set('page.media[element.locale]', data.media)
... because the keyPath which is a string is intelligently parsed by Vue's $set method and is of course it knows that this path is relative to the $data object.
new Vue({
el: '#app',
data() {
return {
msg: "hello world",
attr: {
lang: {
zh: '中文',
en: 'english'
}
}
}
},
methods: {
$set2(obj, propertyName, value) {
let arr = propertyName.split('.');
let keyPath = arr.slice(0, -1).join('.');
let key = arr[arr.length - 1];
const bailRE = /[^\w.$]/
function parsePath(obj, path) {
if (bailRE.test(path)) {
return
}
const segments = path.split('.')
for (let i = 0; i < segments.length; i++) {
if (!obj) return
obj = obj[segments[i]]
}
return obj
}
let target = parsePath(obj, keyPath);
// console.log(target, key);
// target[key] = value;
this.$set(target, key, value);
}
},
mounted() {
setTimeout(() => {
// this.$set('attr.lang.zh', '嗯');
// this.$set2(this, 'attr.lang.zh', '嗯');
this.$set2(this.attr, 'lang.zh', '嗯');
}, 1000);
}
})
调用示例:this.$set2(this.attr, 'lang.zh', '嗯');
i have also experienced similar problems,remove variables -,these variables nl-NL, de-NL change to nlNl, deNl
and i not use
obj.$set('page.media[element.locale]', data.media)
but
obj.$set('page.media.'+element.locale, data.media);
then it work

Categories

Resources