VueX Mutation Optimization - javascript

I am working with a VueX store at the moment, and I have 1 mutation that does a push to entire array. Everything works currently, but want to know if I can make this code simpler, less lines / more optimized at the same time.
Side note, these are objects stored in an array.
PUSH_TO_ALL: (state, data) => {
const found = state.all.find((one, i) => {
if (one.slug === data.slug) {
state.all[i] = data // modify current state
return true
}
return false
})
if (!found) {
state.all.push(data) // add to the state
}
}

My first thought is that if you compare the slug in data to that in every object in the array, it must be unique (otherwise you would replace multiple objects in the find).
This means that you can almost certainly make things a lot faster and a lot simpler if you switch from having the 'root' of state be an array, to using an object instead, indexed by slug.
Then your code would switch to being something like:
PUSH_TO_ALL: (state, data) => {
state.all[data.slug] = data
This has 2 advantages - it is much simpler and faster to modify state, since you don't need to walk all of state checking if the object already exists. And secondly there's no need for separate code to distinguish between adding a new object, and replacing it if it already exists.
If for some reason you have to store state as an array, I would use a different part of state to maintain an object which tracks slug to array index. Then your code would be something like:
PUSH_TO_ALL: (state, data) => {
if (state.map[data.slug]) {
state.all[state.map[data.slug]] = data
} else {
// Push returns length of array - index is len-1
state.map[data.slug] = state.all.push(data) - 1
}
Note - in Vue2 you may need to use Vue.set() to update nested objects, since otherwise the code may not react to these changes. Vue3 no longer has this limitation.

You could use Array.findIndex instead of Array.find, and pair it with a ternary to make the trivial logic more concise (though not necessarily clearer).
const mutations = {
PUSH_TO_ALL: (state, data) => {
const indexOfMatchingSlug = state.all.findIndex(one => one.slug === data.slug);
state.all[indexOfMatchingSlug] = data ? indexOfMatchingSlug > -1 : state.all.push(data);
}
}
Array.findIndex documentation
JavaScript ternary operator documentation

Related

React not registering state change when setting previous state with slight modifications as the new one

I have the following code :-
const [dice, setDice] = React.useState(allNewDice()) // Array of objects with the following properties: `value` (random digit between 1 and 6), `id` (unique id), `isHeld` (boolean)
function holdDice(id) {
const heldDie = dice.findIndex(die => die.id === id)
setDice(prevDice => {
prevDice[heldDie].isHeld = !prevDice[heldDie].isHeld
return prevDice
})
}
The function holdDice is working as expected. It updates prevDice's first element's isHeld property to the opposite of what was previously, as evident from logging it to the console. But the only problem is, the state is not getting updated.
So I tried making a copy of prevDice and then returning the copy (using the spread (...) operator) :-
const [dice, setDice] = React.useState(allNewDice()) // Array of objects with the following properties: `value` (random digit between 1 and 6), `id` (unique id), `isHeld` (boolean)
function holdDice(id) {
const heldDie = dice.findIndex(die => die.id === id)
setDice(prevDice => {
prevDice[heldDie].isHeld = !prevDice[heldDie].isHeld
return [...prevDice]
})
}
And this works perfectly.
Can anyone please explain why this behaviour? Why can't I return the previous state passed in to be set as the new state, even when I am not returning the previous state, exactly as it was?
I understand that it's expensive to re-render the component and that maybe React does not update the state when the previous and the new states are equal. But here, the previous and the new states are not equal.
My first guess was that it might have been related to React not deep checking whether the objets of the array prevDice have been changed or not, or something like that. So I created an array of Numbers and tried changing it's first element to some other number, but unless i returned a copy of the array, the state still did not change.
Any help would be greatly appreciated. Thank you!
The first time you call setDice() after the initial mount of your component or rerender, prevDice refers to the same array in memory that your dice state refers to (so prevDice === dice). It is not a unqiue copy of the array that you're able to modify. You can't/shouldn't modify it because the object that prevDice refers to is the same object that dice refers to, so you're actually modifying the dice state directly when you change prevState. Because of this, when the modified prevDice is used as the value for setDice(), React checks to see if the new state (which is prevState) is different from the current state (dice) to see if it needs to rerender. React does this equality check by using === (or more specifically Object.is()) to check if the two arrays/objects are the same, and if they are, it doesn't rerender. When React uses === between prevState and dice, JS checks to see if both variables are referring to the same arrays/objects. In our case they are, so React doesn't rerender. That's why it's very important to treat your state as immutable, which you can think of as meaning that you should treat your state as readonly, and instead, if you want to change it, you can make a "copy" of it and change the copy. Doing so will correctly tell React to use that new modified copy as the new state when passed into setDice().
Do note that while your second example does work, you are still modifying your state directly, and this can still cause issues. Your second example works because the array you are returning is a new array [], but the contents of that array (ie: the object references) still refers to the same objects in memory that your original dice state array referred to. To correctly perform your update in an immutable way, you can use something like .map(), which by nature returns a new array (so we'll rerender). When you map, you can return a new inner object when you find the one you want to update (rather than updating it directly). This way, your array is a new array, and any updated objects are also new:
function holdDice(id) {
setDice(prevDice => prevDice.map( // `.map()` returns a new array, so different to the `dice`/`prevDice` state
die => die.id === id
? {...die, isHeld: !die.isHeld} // new object, with overwritten property `isHeld`
: die
));
}
State doesn't update because you update it yourself in the setDice function.
dice and prevDice refer to the same object so it won't trigger a re-render.
const [dice, setDice] = React.useState(allNewDice())
function holdDice(id) {
const heldDie = dice.findIndex(die => die.id === id)
setDice(prevDice => {
// Create a copy of the dice
// Use whatever method you wish.Spread. JSON parse and stringify.
const currentDice = JSON.parse(JSON.stringify(prevDice))
currentDice[heldDie].isHeld = !prevDice[heldDie].isHeld
return currentDice
})
}

Javascript ES6 Array.prototype.map() and Object.assign() for single line

Assumption (based on tutorials and reading material):
Example; https://redux.js.org/recipes/structuring-reducers/immutable-update-patterns
Updating an object property in an array best practice is through map versus foreach. Every example as per the redux breaks out map into two functions 1. Update object properties/s and return object. Even on medium and other expert content it follows this format
Example:
I'm iterating through an array of objects and updating every boolean active flag based on on whether that object's id property === id argument.
let items = [{id:1,active:false},{id:2,active:false},{id:3,active:false}];
const traditional = (items,id) => items.map(item => {
item.active = item.id === id;
return item
})
console.log("traditional ", traditional(items, 3));
// outcome: [{id:1,active:false},{id:2,active:false},{id:3,active:true}];
Question:
I've been using a combination of Object assign and map to condense the code, but I'm told that is bad practice and should stop. What am I missing i.e. what's the problem with this approach?
let items = [{id:1,active:false},{id:2,active:false},{id:3,active:false}];
const updateItemsById = (items,id) => items.map(item => Object.assign(item, {active: (item.active = item.id === id)}))
console.log("update ", updateItemsById(items, 2));
Updating an object property in an array best practice is through map versus foreach. Every example as per the redux breaks out map into two functions 1. Update object properties/s and return object. Even on medium and other expert content it follows this format
This is generally true only if you really need a separate array. Your current logic is mutating the existing objects, and returning a shallow copy of the array. This is quite an odd thing to do, and is probably the source of why some may be frowning on your existing approach. Usually, you'd want to create a whole new array with new objects, instead of mutating the existing array - use rest syntax instead of Object.assign to keep things more concise:
let items = [{id:1,active:false},{id:2,active:false},{id:3,active:false}];
const updateItemsById = (items,id) => items.map(item => ({
...item,
active: item.id === id
}));
console.log("update ", updateItemsById(items, 2));
If you really intend to change the existing objects, then .map probably isn't appropriate - instead, use a generic iteration method like for or forEach, loop through the objects, and mutate them as needed. for and forEach loops are for generic side-effects.
let items = [{id:1,active:false},{id:2,active:false},{id:3,active:false}];
const traditional = (items,id) => {
for (const item of items) item.active = item.id === id;
};
traditional(items, 3);
console.log("traditional ", items);

Why use the spread operator when calling 'setState()' in React?

I just start picking up react.js so I went through a lot of tutorials and I've stumbled upon this bit which basically meant to delete an item from the state.
this is how the guy introduced to me the delete function
delTodo = id => {
this.setState({
todos: [...this.state.todos.filter(todo => todo.id !== id)]
});
};
Since I am not so familiar with javascript I had a hard time figuring out what the ... operator is doing and why exactly is he using it in the given scenario. So in order to have a better understanding of how it works, I played a bit in the console and I've realised that array = [...array]. But is that true?
Is this bit doing the same exact thing as the one from above?
delTodo = id => {
this.setState({
todos: this.state.todos.filter(todo => todo.id !== id)
});
};
Could someone more experienced clarify to me why he has chosen to be using that approach instead of the one I've come up with?
As per the documentation:
Never mutate this.state directly, as calling setState() afterwards may replace the mutation you made. Treat this.state as if it were immutable.
reactjs.org/docs/react-component.html#state
So, in the example from the tutorial you've mentioned, you wouldn't need to make a copy of the array to update your state.
// GOOD
delTodo = id => {
this.setState({
todos: this.state.todos.filter(...)
})
}
Array.filter method creates a new array and does not mutate the original array, therefore it won't directly mutate your state. Same thing applies to methods such as Array.map or Array.concat.
If your state is an array and you're applying methods that are mutable, you should copy your array.
See more to figure out which Array methods are mutable:
doesitmutate.xyz
However, if you were to do something like the following:
// BAD
delTodo = id => {
const todos = this.state.todos
todos.splice(id, 1)
this.setState({ todos: todos })
}
Then you'd be mutating your state directly, because Array.splice changes the content of an existing array, rather than returning a new array after deleting the specific item. Therefore, you should copy your array with the spread operator.
// GOOD
delTodo = id => {
const todos = [...this.state.todos]
todos.splice(id, 1)
this.setState({ todos: todos })
}
Similarly with objects, you should apply the same technique.
// BAD
updateFoo = () => {
const foo = this.state.foo // `foo` is an object {}
foo.bar = "HelloWorld"
this.setState({ foo: foo })
}
The above directly mutates your state, so you should make a copy and then update your state.
// GOOD
updateFoo = () => {
const foo = {...this.state.foo} // `foo` is an object {}
foo.bar = "HelloWorld"
this.setState({ foo: foo })
}
Hope this helps.
Why use the spread operator at all?
The spread operator ... is often used for creating shallow copies of arrays or objects. This is especially useful when you aim to avoid mutating values, which is encouraged for different reasons. TLDR; Code with immutable values is much easier to reason about. Long answer here.
Why is the spread operator used so commonly in react?
In react, it is strongly recommended to avoid mutation of this.state and instead call this.setState(newState). Mutating state directly will not trigger a re-render, and may lead to poor UX, unexpected behavior, or even bugs. This is because it may cause the internal state to differ from the state that is being rendered.
To avoid manipulating values, it has become common practice to use the spread operator to create derivatives of objects (or arrays), without mutating the original:
// current state
let initialState = {
user: "Bastian",
activeTodo: "do nothing",
todos: ["do nothing"]
}
function addNewTodo(newTodo) {
// - first spread state, to copy over the current state and avoid mutation
// - then set the fields you wish to modify
this.setState({
...this.state,
activeTodo: newTodo,
todos: [...this.state.todos, newTodo]
})
}
// updating state like this...
addNewTodo("go for a run")
// results in the initial state to be replaced by this:
let updatedState = {
user: "Bastian",
activeTodo: "go for a run",
todos: ["do nothing", "go for a run"]
}
Why is the spread operator used in the example?
Probably to avoid accidental state mutation. While Array.filter() does not mutate the original array and is safe to use on react state, there are several other methods which do mutate the original array, and should not be used on state. For example: .push(), .pop(),.splice(). By spreading the array before calling an operation on it, you ensure that you are not mutating state. That being said, I believe the author made a typo and instead was going for this:
delTodo = id => {
this.setState({
todos: [...this.state.todos].filter(todo => todo.id !== id)
});
};
If you have a need to use one of the mutating functions, you can choose to use them with spread in the following manner, to avoid mutating state and potentially causing bugs in your application:
// here we mutate the copied array, before we set it as the new state
// note that we spread BEFORE using an array method
this.setState({
todos: [...this.state.todos].push("new todo")
});
// in this case, you can also avoid mutation alltogether:
this.setState({
todos: [...this.state.todos, "new todo"]
});
As .filter gives you a new array (than mutating the source array), it is acceptable and results in the same behaviour, making spreading redundant here.
What's not acceptable is:
const delIndex = this.state.todos.findIndex(todo => todo.id !== id);
this.state.todos.splice(delIndex, 1); // state mutation
this.setState({
todos: this.state.todos
});
slice is fine though:
const delIndex = this.state.todos.findIndex(todo => todo.id !== id);
this.setState({
todos: [
...this.state.todos.slice(0, delIndex),
...this.state.todos.slice(delIndex + 1)
]
});
If you mutate state (which keeps ref same) React may not be able to ascertain which part of your state actually changed and probably construct a tree on next render which is different from expected.
The spread operator that the guy's code is applying causes the array returned from the filter function to be copied again.
Since [.filter is returning a new array][https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter], you will already avoid mutating the array in state directly. It seems like the spread operator may be redundant.
One thing I wanted to point out too is while the values in a copied array may be the same as the values in an old array (array = [...array]), the instances change so you wouldn't be able to use '===' or '==' to check for strict equivalency.
const a = ['a', 'b']
const b = [...a]
console.log(a === b) // false
console.log(a == b) // false
console.log(a[0] === b[0]) // true
console.log(a[1] === b[1]) // true
Hope this helps!

Redux-persist react native AsyncStorage Object Serialization Objects Not Equal

I am using redux persist to automatically persist and rehydrate the state on application launch as described in the docs, specifically using AsyncStorage: https://github.com/rt2zz/redux-persist.
I have a reducer defined below which keeps the current products added to the shopping cart in state.products
case 'ADD_PRODUCT':
let addedProducts = [...state.products]
addedProducts.push(action.product);
return {
...state,
count: ++state.count,
products: addedProducts
};
case 'REMOVE_PRODUCT':
let count = state.count;
let removedProducts = [...state.products];
let idxOfProduct = state.products.indexOf(action.product);
if(idxOfProduct != -1){
count = --state.count;
removedProducts.splice(idxOfProduct,1);
}
return{
...state,
count: count,
products: removedProducts
};
#1. If I dispatch 'ADD_PRODUCT', it adds the product and then if I dispatch 'REMOVE_PRODUCT' it removes the item as expected.
#2.1 If I dispatch ADD_PRODUCT and then RELOAD my app, the state.products is rehydrated as expected and contains the recently added product.
#2.1.However attempt to call REMOVE_PRODUCT (exactly the same way I called REMOVE_PRODUCT in #1 above) after a I have RELOAD the app. Even though state.products contains the product state.products.indexOf(action.product); returns back -1 and as a result it is not removed.
Why does IndexOf method in #1 work correct as expected when REMOVE_PRODUCT is called. However if I add a product(ADD_PRODUCT) then reload my app and call REMOVE_PRODUCT, IndexOf returns -1 even though it is present in the state.products
I think the problem may be related to the way indexOf treats object equality.
Without reloading, you are adding and removing the same object reference, which is OK.
When you reload, the reference loaded in state.products is different from the one in action.product, so indexOf cannot find it and never returns the index.
To fix this I would use the product id to find that product in the state.products array instead of trying to find the whole object.
To illustrate a bit my answer, this is what you are doing:
var a = {obj: 0};
var b = [a];
b.indexOf({obj: 0}); // -1 not found
This is what you should do:
var a = {id: '26833', obj: 0};
var b = [a];
b.findIndex(function(el){ //findIndex is not supported in IE, find a polyfill for it
return el.id === '26833'
}); //0
This happens, because indexOf uses a strict reference equality check to find the element within the array. This means that it's not enough for the objects to have the same fields and values: it needs to be the very same object. After the app has been reloaded, this can never be true, since the original object has been destroyed.
If your products have some sort of unique ID field, the easiest way to do this would be to filter the list to exclude the item with a matching id:
const products = state.products.filter(p => p.id !== action.product.id);
const count = products.length;
return { ...state, products, count };

Handling state and arrays of objects in React

We all know the React docs say to never mutate this.state directly. I guess I have a lingering question on state arrays and immutability:
If I have an array of objects held in state, should I always use the immutability helper as in this question when mutating that array in any way?
Or is it perfectly acceptable to use [].concat() or [].slice() as answered here and here?
I ask this question again because [].concat() and [].slice() do return new arrays, but only shallow copy arrays. If I change an element of the array, it will change the array in state as well, violating the first rule of Reactjs State (I've been watching too much FMA:Brotherhood):
var arr1 = [{ name : "bill" }, { name : "chet" }];
var arr2 = arr1.slice();
// this changes both arrays
arr2[0].name = "kevin";
// check
(arr1[0].name === arr2[0].name) // => true; both are "kevin"
(arr1[0] === arr2[0]) // => true; only shallow copy
// this changes arr2 only
arr2.push({ name : "alex" });
// check again
(arr1.length === arr2.length) // => false;
(arr1[0].name === arr2[0].name) // => still true;
(arr1[0] === arr2[0]) // => still true;
I understand that the addon update is most commonly used when overriding shouldComponentUpdate, but for what I'm doing I don't need to override that function; I just need to mutate objects in the array held in state by either adding new elements to the array (solved using concat or slice), or by changing properties of existing elements (solved by using React.addons.update).
TL;DR
If not overriding shouldComponentUpdate, when should I use React.addons.update over [].slice() or [].concat() to mutate an array of objects stored in state?
You primarily need to consider boundaries. If you only use this array in the component that owns it, and optionally pass it down as props: it doesn't matter. You can modify it in any way, and unless the other components have some serious problems, they'll never know the difference because they get the new props when your component updates its state.
If you pass this array to some kind of api, such as a flux action creator or a function passed to you as a prop, then you might run into some very serious and hard to track down bugs. This is where immutability is important.
For example, in this case you might want to check some property of a person in the callback, however if it's possible for people[0].name to have changed, you have a race condition.
function savePeople(people){
$.post('/people/update', people, function(resp){
if (people[0].name === ...) {
// ...
}
});
}

Categories