Restrict vue/vuex reactivity - javascript

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

Related

vuex mutation doesn't update DOM

I'm rendering a list based on an object. I'm adding new elements to this object by help of the mutations. When I log this.$store.state.myObject to console, I can see that it is updating. However, my list is not updated.
Actually, I got lucky and I fixed this issue by adding the line of code below in my mutation. (I found out that this can help DOM update.)
But I'd like to learn if this is a good solution.
state.realms = Object.assign({}, state.realms)
Here is my whole mutation:
addToRealms: (state, val) => {
var id = val.id
var realmName = val.name.en_US
state.realms[id] = { name: realmName }
state.realms = Object.assign({}, state.realms)
}
And here is my vue page code
<q-btn flat
v-for="(realm,code) in realms"
:key="code"
#click="selectRealm(realm)"
:label="realm.name"
clickable/>
I defined realms as a computed property.
computed: {
realms () {
return this.$store.state.realms
}
Further info:
I use vue.js devtools extension, when I track my commit on vuex store I can see that I'm really changing the state. But It doesnt affect immediately. If I press commit all button, my list gets updated.
Thanks.
When you use
state.realms = Object.assign({}, state.realms)
you basically are recreating a new object based on the previous one (breaking the references in case there are not nested objects), and that's the reason why in this way the list get updated.
Give a read to this article to understand more about deep-cloning and breaking references.
The Object.assign() method is used to copy the values of all enumerable own properties from one or more source objects to a target object
When you add new properties to the object, those are not reactive. To make them reactive, use the Vue.set() method, (docs here).
Vue.set(state.realms[id],'name', realmName);

Is it bad to use complex objects in react state?

I am writing an application with React, it has some data stored in a component's state.
I initially chose to wrap the data in a function, this function would encapsulate all the actions that one could do on the data.
To keep this question generic, I will show my examples as a todo. My real-world use-case is more complex.
function Todo(todo = EmptyTodo) {
// helper function to easily create immutable methods below
function merge(update) {
return Todo(Object.assign({}, todo, update))
}
return {
getComplete() {
return todo.complete
},
getText() {
return todo.text
},
toggleComplete() {
return merge({ complete: !todo.complete })
},
setText(text) {
return merge({ text })
}
}
}
This example falls short a bit. A better example might ideally be more than just getters and setters. It would probably contain something closer to business logic.
Moving on, now I used the Todo in a react component like this:
class TodoRow extends React.Component {
state = {
todo: Todo()
}
handleToggleComplete = () => this.setState(state => ({
todo: state.todo.toggleComplete()
}))
handleTextChange = e => {
const text = e.target.value
this.setState(state => ({
todo: state.todo.setText(text)
}))
}
render() {
return <div>
<input
type="checkbox"
onChange={this.handleToggleComplete}
value={this.state.todo.getComplete()}
/>
<input
type="text"
onChange={this.handleTextChange}
value={this.state.todo.getText()}
/>
{this.state.todo.getText()}
</div>
}
}
Please forgive the contrived and simplistic example. The point is that the state is no longer a simple key-value store. It has a complex object that attempts to hide the primitive data and encapsulate the business logic (I know there is no business logic in the above example, just imagine it with me).
I do not need to serialize this state. No time travel or restoring state in localstorage.
Can anyone help me understand why this could be a bad practice?
Pros I can think of-
If a todo wants to be used elsewhere, the data logic can be reused.
todo object can enforce/implement immutability in one place.
The separation between display and data operations.
Can implement polymorphic behavior with the Todo object now (ReadOnlyTodo that can't be toggled or
edited, CompleteOnly todo that can be completed but text can't be
edited) without modifying the React component.
Cons I can think of-
Cannot serialize the state as JSON (easily fixed with a Todo.toJSON() method).
Doesn't follow normal pattern where setState operates on a key/value pair.
Calling todo.setText() might be confusing since it doesn't update state.
Codepens: Key/Value Store Complex object
It may not be a good idea because your implementation of immutability is simply, recreating a new object completely with your changes. Also, you are making a shallow copy, and in real-life applications you will realize that nested objects are a common practice, especially in JSON.
Libraries such as Immutable.js create internal hash tables for their data structures that only recreate changed nodes instead of recreating the whole object while abstracting this logic to allow efficient immutability.
As an example, let's say you have an immutable object with 200 properties. For every update in every property, a new object with these 200 properties needs to be recreated. Note that depending on your application, a simple type on an input could recreate the complete object.
// ❗️ Merge would need to reassign all 200 properties for every change.
// ❗️ Plus, it won't work for nested properties as it is a shallow copy:
function merge(update) {
return Todo(Object.assign({}, todo, update))
}
Instead, Immutable.js would, as an example, create a Hash Map with 20 nodes, each node containing 10 properties. If you update one property, only one node is dumped and recreated while the other are kept.
Also, consider that if any child components depend on this said state, their renders will probably be triggered as there is no memoization. That is why libraries such as reselect for Redux exist.
Note: immer.js is a newcomer immutability library that many devs are vouching for, but I can't say much about how it works internally as I don't know it that well.

Vuex: working with complex objects and state-changing functions

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)

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).

React - Changing the state without using setState: Must avoid it?

My code works, but I have a best practice question: I have an array of objects in the state, and a user interaction will change a value of one object at a time. As far as I know, I'm not supposed to change the state directly, i should always use setState instead. If I want to avoid that with any price, I will deep clone the array by iteration, and change the clone. Then set the state to the clone. In my opinion avoiding to change the state that I will change later anyway is just decreasing my performance.
Detailed version:
this.state.data is an array of objects. It represents a list of topics in a forum, and a Favorite button will toggle, calling clickCollect().
Since I have an array in the state, when I change the is_collected property of one item, I need to create a copy of the array to work with, and after changing to the new value, I can set it to the state.
var data = this.state.data.slice(0);
data[index].is_collected = !data[index].is_collected;
this.setState({data: data});
var data = this.state.data : This would copy the pointer to the array and push(), shift(), etc would alter the state directly. Both data and this.state.data will be affected.
var data = this.state.data.slice(0) : This makes a shallow clone, push and shift doesn't change the state but in my clone I still have pointers to the elements of the state's array. So if I change data[0].is_collected, this.state.data[0].is_collected gets changed as well. This happens before I call setState().
Normally I should do:
var data = [];
for (var i in this.state.data) {
data.push(this.state.data[i]);
}
Then I change the value at index, setting it to true when it's false or false when it's true:
data[index].is_collected = !data[index].is_collected;
And change state:
this.setState({data: data});
Consider my array is relatively big or enormously big, I guess this iteration will reduce the performance of my APP. I would pay that cost if I knew that it is the right way for any reason. However, in this function (clickCollect) I always set the new value to the state, I'm not waiting for a false API response that would say to stop making the change. In all cases, the new value will get into the state. Practically I call setState only for the UI to render again. So the questions are:
Do I have to create the deep clone in this case? (for var i in ...)
If not, does it make sense to make a shallow clone (.slice(0)) if my array contains objects? The changes are being made on the objects inside of the array, so the shallow clone still changes my state, just like a copy (data = this.state.data) would do.
My code is simplified and API calls are cut out for simplicity.
This is a beginner's question, so a totally different approach is also welcome. Or links to other Q & A.
import React from 'react';
var ForumList = React.createClass({
render: function() {
return <div className="section-inner">
{this.state.data.map(this.eachBox)}
</div>
},
eachBox: function(box, i) {
return <div key={i} className="box-door">
<div className={"favorite " + (box.is_collected ? "on" : "off")} onTouchStart={this.clickCollect.bind(null, i)}>
{box.id}
</div>
</div>
},
getInitialState: function() {
return {data: [
{
id: 47,
is_collected: false
},
{
id: 23,
is_collected: false
},
{
id: 5,
is_collected: true
}
]};
},
clickCollect: function(index) {
var data = this.state.data.slice(0);
data[index].is_collected = !data[index].is_collected;
this.setState({data: data});
}
});
module.exports = ForumList;
Personally I don't always follow the rule, if you really understand what you are trying to do then I don't think it's a problem.
var data = this.state.data.slice(0);
data[index].is_collected = !data[index].is_collected;
this.setState({data: data});
In this case, mutating state and calling the setState again like this is fine
this.state.data[index].is_collected = !this.state.data[index].is_collected;
this.setState({data: this.state.data});
The reason you should avoid mutating your state is that if you have a reference to this.state.data, and calling setState multiple times, you may lose your data:
const myData = this.state.data
myData[0] = 'foo'
this.setState({ data: myData })
// do something...
// ...
const someNewData = someFunc()
this.setState({ data: someNewData })
myData[1] = 'bar' // myData is still referencing to the old state
this.setState({ data: myData }) // you lose everything of `someNewData`
If you really concerned about this, just go for immutable.js
Muting the state directly breaks the primary principle of React's data flow (which is made to be unidirectional), making your app very fragile and basically ignoring the whole component lifecycle.
So, while nothing really stops you from mutating the component state without setState({}), you would have to avoid that at all costs if you want to really take advantage of React, otherwise you would be leapfrogging one of the library's core functionalities.
If you want follow react best practices, you should do shallow copy of all your array, when you change any property. Please look into "immutable" library implementation.
But, from my experience, and from my opinion, setState method should be called if you have "shouldCompomenentUpdate" implementations. If you think, that your shallow copy will be consume much more resources, then react virtual dom checks, you can do this:
this.state.data[0].property = !this.state.data[0].property;
this.forceUpdate();
If I understood your question right, you have an array of objects and when a property of a single object in array changes,
Create a deep clone of the array and pass to setState
Create a shallow clone and pass to setState
I just checked with the redux sample todo app and in case of a single property of an object changes you've to create a fresh copy of that single object not the entire array. I recommend you to read about redux and if possible use to manage the state of your app.

Categories