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 };
Related
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
})
}
I don't quite understand this function, like what do you filter out and the way it work...can anybody help me to expalin this? I'm a begginer and i need your help! Thank you so much btw!
Function:
removeFromCart =(product) => {
const cartItems = this.state.cartItems.slice()
this.setState({
cartItems: cartItems.filter((x)=>x.id!==product.id)
})
}
Array.prototype.filter expects a callback that returns a boolean.
If the callback returns true, the element gets pushed to the new array and if it returns false, the element will not be pushed.
const cartItems = this.state.cartItems.slice() creates a new array and can be removed as filter creates a new array anyway.
cartItems.filter((x)=>x.id!==product.id)
If we consider the above snippet, the returned value is x.id!==product.id because if the id matches, x.id !== product.id will evaluate to false and returning false will remove the product.
Update:
You also seem to have an issue with passing the product to the function. Fixed that and here's the updated sandbox:
<button className="button" onClick={() => props.removeFromCart(item)}>
Remove
</button>
This is the longer version of that code, you might get confused because your version has a lot of shortcuts and bad variable name. (comments)
removeFromCart = (product) => {
const cartItems = this.state.cartItems.filter((item) => {
return item.id !== product.id;
});
this.setState({
cartItems: t
})
}
let's know this:
const cartItems = this.state.cartItems.slice()
First, you don't need this one as filter already returns a new array
usually you use slice to create a copy (portion) of that array (usually we pass arguments on it and I've rarely seen a usage where the dev uses it as a "clone" function. You can read more about it here.
const cartItems = this.state.cartItems.filter((item) => {
return item.id !== product.id;
});
for this one, I renamed your variable x into item to make it more understandable. Since cartItems is an array, this function will go through each items then create a new array where the item id is not the same as product id (return item.id !== product.id). Read more about filters here
this.setState({
cartItems
})
you pass the newly created cartItems to update your state.
In you code the Array.prototype.slice function is use to create a copy of the state cartItems which is an array. the Array.prototype.filter function is use to filter the array and return only item which match the condition which is test in the callback function passed to the filter function.
Precisely you are setting the cartItems array to be equal to an array which contains only item which id is different than the id of the product pass as argument of the removeFromCart function.
There are two things to note.
First of all, we are taking a shallow copy of the array by using slice() method of javascript. This is done so that
we do not mutate the original state
(Which is a foundational principle in React)
const cartItems = this.state.cartItems.slice(); // We have the copy of original state
The same can we achieved with ES6 spread operator though (which is recommended in React)
const cartItems = [...this.state.cartItems];// This does the same what slice() does
Second part is the logic in filter method which is a Higher Order Function.
cartItems.filter((x)=>x.id!==product.id)
For each item in your array (Which is each object in your array), this callback function runs
(x)=>x.id!==product.id
in each iteration and checks whether item.id (i.e Current Object's id) is not equal to the Id selected. So, All the items are filtered and shown expect for the item where product's id matches the item's id (This one is not shown in DOM) all other.
Finally, you are updating your state with the filtered items and hence your functionality works as expected :)
Would recommend you to learn about Higher Order Functions, such as map(), filter(), reduce() since they are used a lot in React and Javascript frameworks, since you are a just starting.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter
In your Cart.js, you have to change call function and pass item, props.removeFromCart(item)}
<button
className="button"
onClick={() => props.removeFromCart(item)}
>
Remove
</button>
and in App.js, keep same or,
removeFromCart = (product) => {
//const cartItems = this.state.cartItems.slice();
this.setState((prevState) => ({
cartItems: prevState.cartItems.filter((x) => x._id !== product._id)
}));
};
this do exactly what needs to be done, but it removes selected cart elements
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
I have an app where the redux state has a field called data that's null initially, then when uploading something it changes to an array of objects and the reducer uses delete to remove an object when we press the required button. But that doesn't change the length of the array so it causes an uncaught error. The simplest solution would be to simply change the length of the array after we delete that object. Can this produce any bugs?
EDIT: This is the code:
case actions.RIR:
const filtered = [...state.data];
delete filtered[action.payload];
filtered.length--; // this has been added by me
return {...state, data: [...filtered]};
The original code isn't mine. It's part of a project. I've been asked to fix an error.
First one foremost, reducers should be pure - meaning - no side effects.
delete is a side effect since it mutates an original object.
In reducers, use immutable patterns - either use filter, or splice a copy of the original array (less recomended).
Besides that, delete will simply make this element undefined, and your array will look like:
['item 1','item 2'....,empty,'item n']
I would write it:
case actions.RIR:
const filtered = state.data.filter(item => item !== action.payload);
return {...state, data: filtered};
Or whatever filter logic you might require.
I am trying to delete a property name from the array of object, it's working properly using filter API,
const users = [
{ name: 'Tyler', age: 28},
{ name: 'Mikenzi', age: 26},
{ name: 'Blaine', age: 30 }
];
const myProp = users.filter(function (props) {
delete props.name;
return true;
});
console.table(myProp);
const myProp2 = users.reduce((people, user) => {
console.log(people);
console.log(user);
delete user.name;
return people;
}, []);
console.log(myProp2);
The same example before I am trying complete using reduce API, However, it's not working as expected.
It's not working because your not pushing to the previous element (you are always returning the empty array). You need to change it to:
const myProp2 = users.reduce((people, user) => {
delete user.name;
people.push(user)
return people;
}, []);
Please note that is not the intended use for reduce though - map is the operation you are looking for:
const myProp2 = users.map(u=> ({age: u.age}));
You actually want to use map for this, because you are selecting a transormation of the data into a new object (similar to Select in SQL or LINQ)
const myProps = users.map(u=> ({age: u.age}))
Also although the filter method worked, this is actually abuse of the filter method. The filter method is supposed to remove elements from the array depending on a condition. Your method worked because you returned true (which removed no elements) but you modified the current value on each iteration.
This is bad practice because you will confuse the next person to look at your code, they will wonder why you used filter as a method to transform the data rather than map.
Also don't use reduce because reduce is an aggregation function intended to perform aggregate functions on objects. Since the number of elements you are returning will be the same, map is better for this.
Reduce would be better suited for if you wanted to find out the average,max,min,median age or the most popular name etc...