I have a getter in VUEX and i'm trying to filter an array inside an array but keep getting a warning about modifying state inside the getter.
Error: [vuex] do not mutate vuex store state outside mutation handlers
I know i can do a simple filter on the top level array but it doesn't seem to work on the people array, the only way that i can get it to show the results i want is by doing the following (which is wrong)
for (const company of company.companies) {
const filteredPeople: IPerson[] = company.people.filter(
x => x.jobId === 1
);
company.people = filteredPeople;
}
In short, you cannot modify state inside getters as it would easily result in an endless loops. Besides, getters are not meant to ever modify any kind of outer state, but rather just return (perhaps translated) piece of state for further data presentation. Try this piece of code, instead of modifying state you are returning translated copies of companies with filtered people:
return company.companies.map(c => ({...c, people: c.people.filter(p => p.jobId === 1)}))
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'm currently trying to add a React component into the third layer of my multidimensional array. My structure is as follows:
constructor(props) {
super(props);
/**
* The state variables pertaining to the component.
* #type {Object}
*/
this.state = {
structure : [ // Block
[ // Slot 1
[ // Layer 1
<Text>text 1</Text>, // Text1
]
]
],
};
}
this.state.structure contains an array for the slots, which contains an array for the layers which contains an array consisting of Text components.
I've tried to use both concat() and push() to try to set the structure array, but both give me errors like "cannot read property of undefined", etc. This is my current addText() function:
addText() {
this.setState((previousState) => ({
structure : previousState.structure[0][0].concat(<Text>text2</Text>),
}));
}
The addText() function is called upon pressing a button in my application.
I'm also rendering my array of Text components on the current layer directly through the array:
{ this.state.structure[0][0] }
I feel like I'm staring the problem directly in the face, but have no idea what's causing the issue. I'm expecting to be able to add another Text to the container, but nothing I've tried to do seems to be working.
For one thing, you really should NOT store instances of React components directly in state. Doing so is just asking for trouble down the line, as rendering, state updates, key management and data persistence get more and more difficult to wrangle.
Instead, store a model of your components in state, generally in the form of the props they should have. Then, at render time is when you translate those models into React components:
this.state = {
structure : [ // Block
[ // Slot 1
[ // Layer 1
"text 1", // Text1
]
]
],
};
render() {
return this.state.structure[0][0].map(ea => <Text key={ea}>{ea}</Text>);
}
Doing it this way is better because: what happens if you want to read or modify the contents of your "layer" arrays? If they are text values you can simply read/modify the text. But if they are instantiated React components... there is basically no way to effectively update your state without jumping through tons of hoops.
On to your specific problem: the issue is that when you evaluate a statement like this:
previousState.structure[0][0].concat(<Text>text2</Text>)
the concat() function actually returns the value of the array after concatenation. And the value of that array is (in this case) now [<Text>text 1</Text>, <Text>text2</Text>]. So, you are actually updating the value of your state.structure field to an entirely different array. I'm guessing the error you are seeing - "cannot read property of undefined" - is because when you try to access this.state.structure[0][0] you are trying to access it as if it were a two dimensional array but it is now in fact a one dimensional array.
As another replier put it much more succinctly: you are entirely replacing state.structure with the contents of state.structure[0][0] (after concatenation).
Updating deeply nested data structures in React state is super tricky. You basically want to copy all of the original data structures and change only parts of it. Fortunately, with ES6 "spread" operator we have a shorthand for creating new arrays with all of the same items, plus new ones:
addText() {
this.setState((previousState) => {
// Copy the old structure array
const newStructure = [...previousState.structure];
// Update the desired nested array by copying it, plus a new item
newStructure[0][0] = [...newStructure[0][0], "text2"];
this.setState({structure: newStructure});
});
}
Main takeaways:
Don't store React components in state! Store raw data and use it at render time.
Be careful about what you are updating state to - it needs to be the SAME state plus a few updates, not the result of some atomic operation.
Read up a bit on how array operations like concat() and push() actually work and what their return values are.
I dont agree with this arr structure, and hooks, with an object seems much more conventional, but it could be your this binding. as ray said below structure[0][0] is replacing the state.
if your using addText Method it needs to be bound to the class. If its not using arrow syntax, it will show as undefined as it is not bound, and will result in undefined. Hope it helps
for example
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
value: '',
};
this.addText = this.addText.bind(this);
}
addText() {
this.setState((previousState, props) => ({
structure : previousState.structure[0][0].concat(<Text>text2</Text>),
}));
}
// ES6
addTextEs6 = () => {
this.setState((previousState, props) => ({
structure : previousState.structure[0][0].concat(<Text>text2</Text>),
}));
}
}
//ES6
/* Also you do not need to use concat you can use the spread Op. ... See MDN Spread Op`
Without getting into any philosophy about whether you should do it the way you're doing it, the fundamental problem is that you're replacing structure with structure[0][0] in your state.
{
// this line sets the whole structure to structure[0][0]:
structure : previousState.structure[0][0].concat(<Text>text2</Text>),
}
Currently I've a react function that removes from a Array called rents the current rent perfect. The issue is that I need to update the rent row value called status and set property from 1 to 4 the code below works. I don't seem to get how to get the Index of rent to be able to update it.
removeItem (itemIndex) {
this.state.rents.splice(itemIndex, 1) // removes the element
this.setState({rents: this.state.rents}) // sets again the array without the value to the rent prop
console.log(itemIndex) // itemIndex
}
currently I'm adding this to the code to debug but get this error
console.log(this.state.rents[itemIndex].unique_key)
Stack Trace
TypeError: Cannot read property 'unique_key' of undefined
I need to be able to update the rent property value called status from 1 to 4 and setState again
To elaborate the comments, starting first with the most important:
Like #devserkan said, you should never mutate your state (and props), otherwise you start to see some really weird hard-to-make-sense bugs. When manipulating state, always create a copy of it. You can read more here.
Now for your question:
this.setState is asynchronous, so to get your state's updated value you should use a callback function
const rents = [...this.state.rents]; // create a copy
rents.splice(itemIndex, 1);
this.setState({ rents }, () => {
console.log(this.state.rents); // this will be up-to-date
});
console.log(this.state.rents); // this won't
Personally, I like using the filter method to remove items from the state and want to give an alternative solution. As we tried to explain in the comments and #Thiago Loddi's answer, you shouldn't mutate your state like this.
For arrays, use methods like map, filter, slice, concat to create new ones according to the situation. You can also use spread syntax to shallow copy your array. Then set your state using this new one. For objects, you can use Object.assign or spread syntax again to create new ones.
A warning, spread syntax and Object.assign creates shallow copies. If you mutate a nested property of this newly created object, you will mutate the original one. Just keep in mind, for this situation you need a deep copy or you should change the object again without mutating it somehow.
Here is the alternative solution with filter.
removeItem = itemIndex => {
const newRents = this.state.rents.filter((_, index) => index !== itemIndex);
this.setState({ rents: newRents });
};
If you want to log this new state, you can use a callback to setState but personally, I like to log the state in the render method. So here is one more alternative :)
render() {
console.log( this.state.rents );
...
}
I know how to do this but trying this way and not sure why it wont work?
drawCard = () => {
const deck = this.state.cards;
deck.shift();
console.log(deck, 'deck'); //this is correctly logging one less everytime
this.setState({cards: deck})
}
cards is just an of objects
so even though the function is being called and the console log is working, why is it not updating state?
(console.log(state.cards.length) always returns 3)
Don't use methods that mutate the state (for instance, Array#shift). You could instead use Array#slice to return a shallow copy of a portion of the array:
drawCard = () => {
this.setState({ cards: this.state.cards.slice(1) })
}
The problem arises because you are directly modifying the state. Although you are assigning the state to 'deck' remember that in Javascript arrays and objects are references. So when you are modifying deck you are actually trying to modify the state itself. You can use other methods to create a copy of the states values and modify that.
You may find this helpful:
React - Changing the state without using setState: Must avoid it?
in react, you can't just change the state,
you need to create a copy of your state before updating,
the easiest way to do it is just replace this:
const deck = this.state.cards;
into this:
const deck = [...this.state.cards];
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 === ...) {
// ...
}
});
}