Vuex: working with complex objects and state-changing functions - javascript

Suppose I'm using an external API which works with Machine objects. You can create a Machine with createMachine, which will give you a complex object with several nested properties, and a set of functions to alter state on that object. The API provides for example: loadMemory, sleep, connectDevice. (Imagine anything similar, this is just an example).
I want to mantain a global Vuex Machine object, so I've an action to dispatch that initial creation and store the returned object just like:
actions: {
createChannel({ commit, state }, params) {
m.createMachine(params).then(
function (newMachine) {
commit('setMachine', newMachine);
}
).catch(err => { console.error("Error"); } )
}
}
The mutation is pretty straightforward in this case:
setMachine(state, machine) {
state.machine = machine;
}
Now that API set for the "Machine" objects, as we know has a bunch of state-modifying calls -we don't know what specific fields they change-.
As they modify state, and I want to use them to affect the global Machine object in the Vuex store, I would like to wrap them in actions.
An action could call
this.state.machine.loadMemory(mem.addr)
But if this call itself modified the machine object, how do I commit the new state? Should I clone the old object, apply the state-changing method and replace the object ?
I know cloning is not an easy task.
Thanks.

You can re-mount your complex object. According to the example, the mutation could be:
loadMemory(state, newAddr) {
const { machine } = state;
state.machine = {
...machine, // set all machine's properties
addr: newAddr,
};
}
It works in any level of nested objects you want. Another example:
loadMemory(state, newValue) {
const { machine } = state;
const { machineObjProp } = machine;
state.machine = {
...machine, // set all machine's properties
machineObjProp: {
...machineObjProp, // set all machineObjProp's properties
value: newValue,
},
};
}

One way is using lodash cloneDeep, it will copy app properties and methods of object
import _ from lodash
this.state.machine.loadMemory(mem.addr)
const copyMachine = _.cloneDeep(this.state.machine)
this.$store.commit('setMachine', copyMachine)

Related

Returning copies of objects with the spread operator and effects on performance with Javascript

I wanted to make an object where you could not directly mutate the variables so I decided to create what is essentially a store with a reducer similar to redux. When I return the store with the get() function I am returning a copy of the store instead of the original store so it cannot be mutated directly. There are variables in the store that I want to remain untouched throughout the controller.
My question is are there any serious performance tradeoffs that I should be aware of when making a copy of the object vs returning the entire object. The get() function will be used a lot so many copies will be made. I also use it in a requestAnimationFrame function where the get() function will be called a lot back to back for each animationFrame. I assume that the old copy will be removed from memory when no longer in use but I am not for sure.
Here is a very simple example.
const reducer = (store, action) => {
switch(action.type) {
case 'SET_FOO':
return {
...store,
foo: action.payload
}
case 'SET_BAR':
return {
...store,
bar: action.payload
}
default:
return store
}
}
const createController = () => {
let store = {
foo: 'foo',
bar: 'bar'
}
const get = () => ({...store})
const dispatch = (action) => (store = reducer(store, action))
// A bunch of other irrelevant functions here
return {
get,
dispatch
}
}
As you can see in the get() function I am sending a copy of the store back instead of the actual store. This way it is impossible for the person creating the controller to mutate the store directly. If I just returned the store you could do something like the following and directly mutate the store which I am trying to avoid.
const ctl = createController()
const store = ctl.get()
store.foo = 'baz'
console.log(store.get().foo)
// result would be 'baz'
But if you send back a copy of the store then they could not directly mutate the store but instead you would have to dispatch to the reducer to mutate it. But are there any serious performance concerns that I should take in consideration here. I know adding more objects can use more memory but I don't think it would make a huge difference.
Any insights would be appreciated. Thanks

Mutating data in computed property also changed data in vuex store: why? Any help is appreciated

For my current project, I am working on a shop page. I am sending the data of each order made to my Laravel backend in order for it to be processed. Part of this data is an array with all the ordered products in it. I use JSON.stringify() on this array to avoid errors in the Laravel backend.
My issue is that when I stringify the array in a computed property, this also changed the data in my Vuex store for some reason. This in turn obviously causes quite a bit of error. Is this normal behavior, or am I doing something wrong? How can I avoid this from happening? Thanks in advance!
You can see my component's code in the snippet below.
import { mapGetters } from "vuex";
export default {
computed: {
...mapGetters({
order: "getOrder",
countries: "getCountries"
}),
orderData() {
let myOrder = this.order;
if (myOrder.products) {
myOrder.products = JSON.stringify(myOrder.products);
}
return myOrder;
},
country() {
return this.countries.find(
country => country.iso_3166_2 === this.order.country
).name;
}
},
methods: {
placeOrder() {
console.log(this.orderData);
}
}
};
</script>
You are assigning to myOrder the reference of this.order, subsequently all your modifications inside it will affect this.order (so you are mutating).
In this case, since you just want to modify products, you can shallow copy this.order just like this:
let myOrder = { ...this.order };
Then, all the properties at the first level will have different pointers, so you can change them without fear of mutations.

How to delete entries from Vuex store using dynamic path as payload for a mutation?

I want to create a mutation for the vuex state, but making it dynamically update the state - to have the payload include a path to the object I want to delete the element from and the key.
Dispatching the action
deleteOption(path, key)
{ this.$store.dispatch('deleteOptionAction', {path, key}) }
Commiting the mutation
deleteOptionAction ({ commit }, payload) { commit('deleteOption', payload) }
The mutation receives the payload with path = 'state.xmlValues.Offers[0].data.Pars' and key = 0
deleteOption (state, payload) {
let stt = eval('state.' + payload.path)
Vue.delete(stt, payload.key)
// delete stt[payload.key] - works the same as Vue.delete
state.xmlValues.Offers[0].data.Pars = Object.assign({}, Object.values(stt))
}
I have tried to use the state[payload.path] syntax - but this does not work.
The path includes the string 'state.xmlValues.Offers[0].data.Pars', so to make it work, I have used let stt = eval('state.' + payload.path).
But then, to delete an element from the state becomes tricky:
when using Vue.delete(stt, payload.key) - it will only delete the element's key locally stored in stt variable, not in the state.
Then I re-assigned the state objects with the stt (from which the needed element is deleted already), hardcoding the path - and that's what I try to avoid:
state.xmlValues.Offers[0].data.Pars = Object.assign({}, Object.values(stt))
How do I pass a path to the store, then use it to delete an object in the state without hardcoding the path explicitly?
As for my other mutation addOption, I also used the dynamic path to the state object - and it works great when using the dynamic path evaluated in stt
addOption (state, payload) {
let stt = eval('state.' + payload.path)
Vue.set(stt, payload.key, payload.newEl)
}
First things first: Don't use eval(..). Ever. This function allows for arbitrary code execution, and you do nothing to sanitise the value!
A more sane option would be to parse your path yourself. You could write something yourself, but lodash already has the toPath function for that. It returns an array with each part of what we try to get.
Now that we know how to get to the key we want to delete, we could write some code that tests if each part exists and if each part is an object or an array. But, since we are now using lodash, we could make our lives easier by using _.get and _.isObjectLike:
import { toPath, get, isObjectLike } from 'lodash-es';
function deletePath(source, pathStr) {
const path = toPath(pathStr);
const selector = path.slice(0, -1);
const key = path[path.length - 1];
let deletableSource = source;
if (selector.length && isObjectLike(source)) {
deletableSource = get(source, selector);
}
if (isObjectLike(deletableSource)) {
// We can delete something from this!
this.$delete(deletableSource, key);
}
}
Now that we have that, we can do something like assigning it to the Vue prototype, or exporting it as a helper function somewhere. I will rewriting your addOption as a reader's exercise.
Thanks to #Sumurai8, I figured out that it is possible to pass a parameter not as a string but actually a reference to the store like this. So no need to pass the sting with the path to the object in the state .
<button #click="deleteOption($store.state.xmlValues.Offers[mainData.curOfferKey].data.Pars)">delete</button>
The lodash get and toPath function was also very useful!

Costly Immutable Operation

I have a data in my redux state that has like ~100,000 json records (with each record having some 8 key value pairs).
I have a logic written on client side to refresh this data every 30 seconds (every 30 seconds I make a call to server to get what records to remove and what records to add).
I do this processing in my reducer function, for this I have written a method "mergeUpdates" that iterates through the state.data object identifies what to remove and what to insert.
I was using fromJs(state.data).toJs from immutable to clone the state.data and make an update (state.data is not immutable). But this cloning turned out to be very costly operation (takes around 2 seconds for 100,000 records) hence I removed the cloning and started modifying state.data itself that resulted in "Assignment to function parameter" lint error because I am modifying data that is being passed to a function.
initialState = {
data: {}
}
doSomething(state, change) {
// iterates on state and updates state
state = doSomethingElse();
return state;
}
mergeUpdates(state, change) {
// some logic
state = doSomething(state, change)
// some more logic
return state;
}
export default function (state=initialState, action) {
switch (action.type) {
case REFRESH: {
return mergeUpdates(state.data, action.data)
}
}
}
What is the best practice to handle such cases, should I assign state to a new reference in "mergeUpdates" and "doSomething" methods and return a new reference, or make my data inside state an immutable or something else?
For ex: is this considered a good practice?
doSomething(state, change) {
let newState = state;
// process change and return newState
return newState;
}
mergeUpdates(state, change) {
let newState = state;
// apply change on newState
newState = doSomething(newState, change);
// return newState
return newState;
}
You should avoid using Immutable.js's toJS() and fromJS() functions as much as possible. Per its author Lee Byron, this are the most expensive operations in the library.
I would advise finding a different approach. Some suggestions:
If your data is already in Immutable.js, use the other functions it provides to help update that data (such as Map.withMutations())
If that data isn't already in Immutable.js, or you want to avoid using it, you might want to look one of the many immutable update libraries available, some of which are specifically intended to help merge new values into existing data structures.
Redux itself does not force user to use plain object for state management. You can use immutable object to store data and the only thing you need to do is that re-inits brand new state by fromJS as you did above.
Beside, immutable must be used in entire app. Actually I suggest you use spread operator for data modifying if your data structure is not complicated instead of cumbersome Immutable. (for me, over 3 level down the tree is complicated).

Restrict vue/vuex reactivity

Let's assume we have some array of objects, and these objects never change. For example, that may be search results, received from google maps places api - every result is rather complex object with id, title, address, coordinates, photos and a bunch of other properties and methods.
We want to use vue/vuex to show search results on the map. If some new results are pushed to the store, we want to draw their markers on the map. If some result is deleted, we want to remove its marker. But internally every result never changes.
Is there any way to tell vue to track the array (push, splice, etc), but not to go deeper and do not track any of its element's properties?
For now I can imagine only some ugly data split - keep the array of ids in vue and have separate cache-by-id outside of the store. I'm looking for a more elegant solution (like knockout.js observableArray).
You can use Object.freeze() on those objects. This comes with a (really tiny!) performance hit, but it should be negligible if you don't add hundreds or thousands of objects at once.
edit: Alternatively, you could freeze the array (much better performance) which will make Vue skip "reactifying" its contents.
And when you need to add objects to that array, build a new one to replace the old one with:
state.searchResults = Object.freeze(state.searchResults.concat([item]))
That would be quite cheap even for bigger arrays.
At the second glance data split seems not so ugly solution for this task. All that we need is using getters instead of the raw vuex state. We suppose that incoming results is an array with any objects that have unique id field. Then the solution could look like:
const state = {
ids: []
}
let resultsCache = {};
const getters = {
results: function(state) {
return _.map(state.ids,id => resultsCache[id]);
}
}
const mutations = {
replaceResults: function(state,results) {
const ids = [];
const cache = {};
(results||[]).forEach((r) => {
if (!cache[r.id]) {
cache[r.id] = r;
ids.push(r.id);
}
});
state.ids = ids;
resultsCache = cache;
},
appendResults: function(state,results) {
(results||[]).forEach((r) => {
if (!resultsCache[r.id]) {
resultsCache[r.id] = r;
state.results.push(r.id);
}
});
}
}
export default {
getters,
mutations,
namespaced: true
}
I created a fork out of vue called vue-for-babylonians to restrict reactivity and even permit some object properties to be reactive. Check it out here.
With it, you can tell Vue to not make any objects which are stored in vue or vuex from being reactive. You can also tell Vue to make certain subset of object properties reactive. You’ll find performance improves substantially and you enjoy the convenience of storing and passing large objects as you would normally in vue/vuex.
You can use shallowRef to achieve this.
First import it:
import {shallowRef} from 'vue';
In your mutations you can have a mutation like this:
mutations: {
setMyObject(state, payload) {
state.myObject = shallowRef(payload.value);
},
}
This will track replacing the object, but not changes to the objects properties.
For completeness here is the documentation to shallowRef:
https://v3.vuejs.org/api/refs-api.html#shallowref

Categories