React Array in State Mutating - javascript

I don't know if there is a conflict in the libraries I'm using but for some reason or its, the latest react but when I'm updating an array from the state, it's mutating it even though I haven't called setState. Is there a special case for Arrays in the state that I'm not aware of?
constructor(props){
super(props);
this.state = {
data: [
{ id: 1, name: 'hello' },
{ id: 2, name: 'world' },
{ id: 3, name: 'once' }
]
}
}
performUpdate() {
const { data } = this.state;
var current_data = data.slice();
current_data[0].name = 'helloooooo';
console.log(data)
// it shows data.id 1 now has name: 'helloooooo'
}
Since I made a copy of the array from state, shouldn't the state be updated only if I call
this.setState({
data: current_data
})

Since I made a copy of the array from state, shouldn't the state be
updated only if I call
You made a shallow copy. This:
current_data[0].name
is same as saying:
this.state.data[0].name
this.state.data[0] and current_data[0] point to same object, due to shallow copy; even though slice returned new array, its elements basically point to same objects as the elements from the original array.
You maybe interested in how to immutably modify one of array elements.

slice performs a shallow copy of the array - meaning that only the pointers to the objects contained in the array are copied, not the objects themselves.
This means that after the slice you have two arrays pointing at the same objects. Naturally, then, changing an object in one, will change it in the other.
Instead, what you want to do is create a deep clone. You can use immutability-helper or something like Lodash to achieve this.

Related

React How to Update State Object Value in State Using SetState

I am trying to update a single object value (boolean) in my array using setState. Based on previous discussions, my approach does not mutate prior state, creates a copy of the original array, updates the copy, and then updates the original array. The 'issue' I have run into is that while the state updates my virtual list, the array does not re-render (FlatList in RN, specifically).
Previous Discussions Referenced:
React: how to update state.item[1] in state using setState?
As a work around, I make a copy of my original array and then set the original array to empty. The cost is a very brief 'flicker' whenever the array re-renders.
Question: Is there a better way to update an object's value in my array AND trigger a re-render of the array elsewhere in my application?
Current Solution:
originalArray = [{ id: '123', randomKey: true }, {'234', randomKey: false }, ...]
const index = originalArray.findIndex(item => item.id === currentItemId)
if (index > -1) {
const newArray = [...originalArray]
setOriginalArray([]) // My solution to re-render the array
let updatedItem = {...newArray[index]}
updatedItem.randomKey = !randomKey // the value is a boolean
newArray[index] = updatedItem
setOriginalArray(newArray)
}
Cheers!

How can I use setState() to modify a multidimensional array in React Native?

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>),
}

Updating State of type Array<Object> in ReactJS without mutation

I have an array of objects with meta information.
Here is the schema for a object.
this.state.slotData [{
availability: boolean,
id: number,
car: {
RegistrationNumber : string,
Color: string
}, {...}, {...}, ...]
Now, for any incoming car, I record car details
I am further checking if any slots are available, and if so, updating the slotData state.
I filter() the slotData to find all available slots then refer availableSlots[0] to access the id of the nearest empty slot.
Now, I just have to update this.state.slotData without mutating it.
If you want to apply immutable update and add car as a single object in slotData array you should first spread whole slotData array and than add car object like this:
const updatedSlotData = [...slotData, carObj];
this.setState({ slotData: updatedSlotData });
As long as you make a copy first, you can safely mutate the copy.

How to update nested states in React, should state be immutable?

We are having a heated discussion on how to update nested state in React.
Should the state be immutable or not? What is the best practice to update the state gracefully?
Say you have state structure that looks like this:
this.state = {
numberOfStudents: "3",
gradeLevel: "5",
students : [
{ id : "1234",
firstName: "John",
lastName: "Doe",
email: "johndoe#mail.com"
phoneNumer: "12345"
},
{ id : "56789",
firstName: "Jane",
lastName: "Doe",
email: "janedoe#mail.com"
phoneNumer: "56789"
},
{ id : "11111",
firstName: "Joe",
lastName: "Doe",
email: "joedoe#mail.com"
phoneNumer: "11111"
}
]
}
Then we want to update joe doe's phone number.
A couple of ways we can do it:
mutate state + force update to rerender
this.state.students[2].phoneNumber = "9999999";
this.forceUpdate();
mutate state + setState with mutated state
this.state.students[2].phoneNumber = "9999999";
this.setState({
students: this.state.students
});
Object.assign, this still mutate the state since newStudents is just a new reference to the same object this.state points to
const newStudents = Object.assign({}, this.state.students);
newStudents[2].phoneNumber = "9999999"
this.setState({
students: newStudents
});
Update immutability helper (https://facebook.github.io/react/docs/update.html) + setState. This can get ugly very quickly if we have address.street, address.city, address.zip in each student object and want to update the street.
const newStudents = React.addons.update(this.state.students, {2: {phoneNumber: {$set:"9999999"}}});
this.setState({
students: newStudents
})
Last line of the react doc for setState states that :
Never mutate this.state directly, as calling setState() afterwards may
replace the mutation you made. Treat this.state as if it were
immutable.
https://facebook.github.io/react/docs/react-component.html
The docs states that we shouldn't use forceUpdate to rerender:
Normally you should try to avoid all uses of forceUpdate() and only
read from this.props and this.state in render().
Why is this the case, what can happen if we mutate state and call setState afterward? Under what circumstances will setState() replace the mutation we made? This is a very confusing statement. Can someone please explain the possible complication of each of the scenario we are using above to set the state.
You state that:
"Object.assign, this still mutate the state since newStudents is just a new reference to the same object this.state points to"
This statement is incorrect.Object.assign mutates the state passed in to its first parameter. Since you pass in an empty object literal ({}), you are mutating the new object literal and not this.state.
Some background:
The principle of Immutable state has connections with Functional programming.
It is useful in React because it provides a way for React to know if
the state has changed at all, one use case it is useful is for optimising when components should re-render
Consider the case of a complex state with nested objects.
Mutating the state's values would alter the values of properties within the state but it would not change the object's reference.
this.state = {nestObject:{nestedObj:{nestObj:'yes'}}};
// mutate state
this.state.nestObject.nestedObj.nestObj= 'no';
How do we know if React should re-render the component?
A deep equality check? Imagine what this would look like in a complex state, that's hundreds, even thousands of checks per state update...
No need to check for changes, just force React to re-render everything with every state change...
Could there be an alternative to the latter two approaches?
The Immutable way
By creating a new object (and therefore a new reference), copying over the old state with Object.assign and mutating it, you get to manipulate the state values and change the object reference.
With the Immutable state approach we can then know if the state has changed by simply checking if the object references are equal.
An simplified example for the naysayers in the comments below:
Consider this simple example:
this this.state = { someValue: 'test'}
var newState = Object.assign({}, this.state);
console.log(newState); // logs: Object {someValue: "test"]
console.log(this.state); // logs: Object {someValue: "test"]
// logs suggest the object are equal (in property and property value at least...
console.log(this.state === this.state); // logs: true
console.log(this.state === newState); // logs: false. Objects are
// pass-by-reference, the values stored
// stored in this.state AND newState
// are references. The strict equality
// shows that these references
// DON'T MATCH so we can see
// that an intent to modify
// state has been made

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