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}
})
Related
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.
So I am having a state somewhat like this
this.state={
angles:{}
}
So how can I do setState on this empty object. For instance if I want to set a key and value inside my empty angles. How can I do that. ( Likewise I want 0:90 inside my this.state.anlges.
After setting the state it should look like
this.state={
angles:{0:90}
}
Thanks in advance. Need to pass both the 0 and 90 as variables.
You'd do it by setting a new angles object, like this if you want to completely replace it:
this.setState({angles: {0: 90}});
or like this if you want to preserve any other properties and just replace the 0 property:
// Callback form (often best)
this.setState(({angles}) => ({angles: {...angles, 0: 90}}));
or
// Using the current state (often okay, but not if there may be other state updates pending)
this.setState({angles: {...this.state.angles, 0: 90}});
In a comment you've asked:
Actually I need to pass 0 and 90 as variables. For instance consider 0 as one variable and 90 as one variable. Then in that case How can i do that?
In the above where I have 0: 90 you can use computed property notation: [propertyname]: propertyvalue where propertyname is the variable containing the property name and propertyvalue is the variable containing the property value. For instance, here's that last example with those variables:
this.setState({angles: {...this.state.angles, [propertyname]: propertyvalue}});
You can write something like this:
this.setState((currentState) => ({
angles: {
...currentState.angles,
0: 90,
}
}));
be aware that number as key in objects is not recommended
if both 0 and 90 are values then angles should be an array containing duos of values.
example:
angles: [[0,90], [60,45], [0, 45]]
to do this within your state you would to something like this:
// initial state:
this.state = {
angles: [],
}
// add a value:
this.setState(({angles}) => ({
angles: angles.concat([[0,90]])
}))
note the double array syntax in concat, it is necessary. Without this you would end up with a flat 1 dimension array
I think this is the simplest way to do it:
this.setState({angles: {0:99}});
You probably want something like this:
const state = {
angles: {}
};
const setThisToState = { 0: 90 };
const key = Object.keys(setThisToState).toString();
const value = Object.values(setThisToState).toString();
state.angels = { [key]: value }; // { angles: { '0': '90' }
EDIT:
Since you asked for having 0 and 90 in variables, I wanted to point out the computed property notation.
class YourClass extends React.Component {
constructor(props) {
super(props);
this.state = {
angles: {}
}
}
componendDidMount() {
// using object destructuring here
const { angles } = this.state;
// say you are fetching data from an API here
fetch(fromAPI)
.then(response => response.json())
.then(data => { // say data.coordinates = { 0: 90 }
const key = Object.keys(data.coordinates).toString();
const value = Object.values(data.coordinates).toString();
// this overrides current state.angles. To keep values of state.angles use
// the spread operator as mentioned below
this.setState({ angles: { [key]: value }})
// spread out current state.angles if you wanna keep the values + add a
// "new object"
this.setState({ angles: ...angles, [key]: value})
})
}
}
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
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.
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);