Avoid state mutation by Object.assign() inside map() - javascript

In componentDidMount i did this:
apps.forEach(app => {
if (chosenAppId) {
if (app.id === chosenAppId) {
this.setState({ map: this.props.map });
}
}
});
and now when i do this in some function:
this.setState({
...this.state.map,
areas: this.state.map.areas.map(el =>
el._id === area._id
? Object.assign(el, {
chooseDevice: false,
editModal: true
})
: Object.assign(el, { chooseDevice: false })
)
});
I have persistent redux state, in this case I would expect that on reload this.state.map === this.props.map but somehow this object.assign mutated my redux state and on reload all is saved to reducer.
I narrowed it down that it has something to do with object.assign because if I .concat() something to this.state.map, that does not changes redux state.
How? I really do not get it. No redux action is dispatched, do not know how this can happen.

The line Object.assign(el, { chooseDevice: false }) will mutate el.
It looks like you copied that from props (and thus likely the Redux store) into state. So, it's the same object reference that was already inside the Redux store, and thus you're mutating the value that's in the store.
Note that our official Redux Toolkit package includes a mutation detection middleware by default that will throw errors when you accidentally mutate values.

You're absolutely right about the root cause of the issue - Object.assign() was mutating your original array items which you were referring to within map().
To resolve this, simply get rid of Object.assign() mutating your state:
this.setState({
...this.state.map,
areas: this.state.map.areas.map(el => ({
...el,
chooseDevice: false,
...(el._id === area.id && {editModal: true})
}))
});

When you spread an object it doesn't clone existing properties, it simply copies them into the new object, meaning instance properties will remain e.g.
const obj = { numbers: [1, 2, 3], person: { name: 'Foo' } }
const copy = { ...obj };
copy.numbers.push(4);
copy.person.name = 'Bar'
console.log(obj.numbers) // [1,2,3,4]
console.log(obj.person) // { name: 'Bar' }
console.log(copy.numbers) // [1,2,3,4]
console.log(copy.person) // { name: 'Bar' }
Notice how the original object has been updated by changes made to the copy
Therefore, when you spread your state into the local state i.e.
...this.state.map
And then use Object.assign on the instance properties, you are inadvertently updating the Redux state at the same time.

With Object.assign()
The Object.assign() method copies all enumerable own properties from one or more source objects to a target object.
Object.assign(target, ...sources)
target
The target object — what to apply the sources’ properties to, which is returned after it is modified.
sources
The source object(s) — objects containing the properties you want to apply.
Object.assign() - JavaScript | MDN
If you'd like it not to alter the element provided, target an empty object. That way el and the additional data in the third argument will be assigned to a new object, instead of overriding properties of el.
this.setState(prevState => ({
...prevState.map,
areas: prevState.map.areas.map(el =>
el._id === area._id
? Object.assign({}, el, {
chooseDevice: false,
editModal: true
})
: Object.assign({}, el, { chooseDevice: false })
)
}));
With object spread (compact)
If you'd like to make ir more compact you can also turn it into an object spread, and use an inline if for the conditional change in editModal.
this.setState(prevState => ({
...prevState.map,
areas: prevState.map.areas.map(el => ({
...el,
chooseDevice: false,
editModal: el._id === area._id ? true : el.editModal
}))
}));
EDIT: this.state should not be used in setState

Related

React setState Array Hook doesn't re-render component [duplicate]

I want to update value of one object only but updating value of one Object, Updates the value for all objects.
let default = {
name: '',
age: ''
}
this.state = {
values: Array(2).fill(default)
}
updateName (event) {
let index = event.target.id,
values = this.state.values;
values[index].name = event.target.value;
this.setState ({
values: values
});
}
There are four significant problems in that code.
You're using the same object for all entries in your array. If you want to have different objects, you have to create multiple copies of the default.
You're calling setState incorrectly. Any time you're setting state based on existing state (and you're setting values based, indirectly, on this.state.values), you must use the function callback version of setState. More: State Updates May Be Asynchronous
You can't directly modify the object held in this.state.values; instead, you must make a copy of the object and modify that. More: Do Not Modify State Directly
default is a keyword, you can't use it as an identifier. Let's use defaultValue instead.
Here's one way you can address all four (see comments):
// #4 - `default` is a keyword
let defaultValue = {
name: '',
age: ''
};
this.state = {
// #1 - copy default, don't use it directly
values: [
Object.assign({}, defaultValue),
Object.assign({}, defaultValue),
] // <=== Side note - no ; here!
};
// ....
updateName(event) {
// Grab the name for later use
const name = event.target.value;
// Grab the index -- I __don't__ recommend using indexed updates like this;
// instead, use an object property you can search for in the array in case
// the order changes (but I haven't done that in this code).
const index = event.target.id;
// #2 - state updates working from current state MUST use
// the function callback version of setState
this.setState(prevState => {
// #3 - don't modify state directly - copy the array...
const values = prevState.values.slice();
// ...and the object, doing the update; again, I wouldn't use an index from
// the `id` property here, I'd find it in the `values` array wherever it
// is _now_ instead (it may have moved).
values[index] = {...values[index], name};
return {values};
});
}
Note that this line in the above:
values[index] = {...values[index], name};
...uses property spread syntax added in ES2018 (and shorthand property syntax, just name instead of name: name).
I would use the Array.prototype.map function with combination of the object spread syntax (stage 4):
Note that i changed the name of the default object to obj.
default is a reserved key word in javascript
let obj = {
name: '',
age: ''
}
this.state = {
values: Array(2).fill(obj)
}
updateName(event){
const {id, value} = event.target;
this.setState(prev => {
const {values} = prev;
const nextState = values.map((o,idx) => {
if(idx !== id)
return o; // not our object, return as is
return{
...o,
name: value;
}
});
return{
values: nextState
}
});
}
There is an easy and safe way to achieve that through the following:
this.setState({
values: [ newObject, ...this.state.values],
});
this will create an instance of the state and change the value of an existing object with new object.

Do I need to use the spread operator when using useState hook on object when updating?

I just started learning about hooks, and according to the official docs on Using Multiple State Variables, we find the following line:
However, unlike this.setState in a class, updating a state variable always replaces it instead of merging it.
So, if I understand correctly, this mean I don't need to use the spread operator for updating the state?
You still don't want to mutate state. So if your state is an object, you'll want to create a new object and set with that. This may involve spreading the old state. For example:
const [person, setPerson] = useState({ name: 'alice', age: 30 });
const onClick = () => {
// Do this:
setPerson(prevPerson => {
return {
...prevPerson,
age: prevPerson.age + 1
}
})
// Not this:
//setPerson(prevPerson => {
// prevPerson.age++;
// return prevPerson;
//});
}
That said, using hooks you often no longer need your state to be an object, and can instead use useState multiple times. If you're not using objects or arrays, then copying is not needed, so spreading is also not needed.
const [name, setName] = useState('alice');
const [age, setAge] = useState(30);
const onClick = () => {
setAge(prevAge => prevAge + 1);
}
What it means is that if you define a state variable like this:
const [myThings, changeMyThings] = useState({cats: 'yes', strings: 'yellow', pizza: true })
Then you do something like changeMyThings({ cats: 'no' }), the resulting state object will just be { cats: 'no' }. The new value is not merged into the old one, it is just replaced. If you want to maintain the whole state object, you would want to use the spread operator:
changeMyThings({ ...myThings, cats: 'no' })
This would give you your original state object and only update the one thing you changed.

How to dynamically set value of an object property in reactJS state?

Let's say a component has state such as:
this.state = {
enabled: {
one: false,
two: false,
three: false
}
}
How can this.setState() be used to set the value of a dynamic property?
For instance, this does not work:
let dynamicProperty = "one"
this.setState({
enabled[dynamicProperty]: true
})
However, this does work, but is also bad practice:
this.enabled = {
one: false,
two: false,
three: false
}
let dynamicProperty = "one"
this.enabled[dynamicProperty] = true;
How can this.setState() be used to accomplish the same thing?
You need to create a copy of the original object and only change the property you want to update. The easiest way to do that is to use the object spread operator:
this.setState(currentState => ({enabled: {...currentState.enabled, one: true}}));
or in a more verbose form:
this.setState(currentState => {
const enabled = {...currentState.enabled, one: true};
return {enabled};
});
If the property name is only known at runtime you can do it like this:
const setEnabled = name => {
this.setState(currentState => ({enabled: {...currentState.enabled, [name]: true}}));
};
The standard practice is to copy the the state, modify the copied state, then set state using that clone, like this:
//with spread operator
const enabledClone = {...this.state.enabled};
enabledClone.one = true;
this.setState({enabled : enabledClone});
You can use braces around an object's key to use a variable to determine the key
const dynamicKey = 'one';
const newObj = {[dynamicKey]: true} //equals {one: true}
Since this.setState only merges on toplevel keys, you will have to create a copy of the current enabled object and use the braces notation:
let dynamicProperty = "one"
this.setState({
enabled: {...this.state.enabled, [dynamicProperty]: true}
})

add row without push in es6 for react state

I'm not sure I'm doing the right thing, I mutate variable outside of setState, it's fine right? or there's more elegant way to do it?
state = {
persons: [{
name: 'jay',
age: 10
}]
}
addRow = () => {
const temp = this.state
temp.persons.push({
name: '',
age: ''
})
this.setState({
...temp
})
}
App demo https://codesandbox.io/s/ppqw4wjqzq
In javascript, object assignment works by referece and hence Even if you mutate the variable outside of setState, it will still refer to the same reference of state as long as you do not clone your object. However if you clone it, a new instance will be created and the original one will not be affected
addRow = () => {
const persons = [...this.state.persons] // Clone it one level deep using spread
persons.push({
name: '',
age: ''
})
this.setState({
persons
})
}
The above can be done using simply spread syntax and functional setState like
addRow = () => {
this.setState(prevState => ({
persons: [...prevState.persons, { name: '', age: ''}]
}))
}
Although in your example there seems no difference between the two actions, there is major flaw in the initial implementation that you provided. In order to see the difference between cloning and pushing and just assigning the reference and pushing, you can see the codesandbox demo.
Basically when you create a new component to which if you pass the state persons as props, and you mutate at its original reference, in the componentWillReceiveProps method, you see that the currentProps and the nextProps are both the same and hence if you have any check in the child component to take action if the persons prop changed, that would fail. Hence its extremely important to not mutate the value at its own reference
Without push and spread syntax, you can still avoid the mutation issue by using concat which create a new copy of the original array
addRow = () => {
this.setState(prevState => ({
persons: prevState.persons.concat([{ name: '', age: ''}])
}))
}
In my opinion, more elegant way would be to use functional setState:
const newPerson = { name: '', age: -1 };
this.setState(prevState => ({ persons: [...prevState.persons, newPerson] })

react wildcard in using setState

I have
this.state = {
modal_1: true,
modal_abc: true,
modal_special: true
}
how can I change everything that start with modal to false? is it possible with
this.setState({
`modal_*`: false
})
There is no such thing as wildcards in React's setState method or javascript's object literal. You can manualy iterate over object keys and reduce it, e.g.:
const newState = Object.keys(this.state).reduce((result, key) => {
// conditionally set value of result
result[key] = key.startsWith('modal_') ? false : this.state[key];
return result;
}, {});
// and set new state
this.setState(newState);

Categories

Resources