可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
I\'m trying to organize my state by using nested property like this:
this.state = {
someProperty: {
flag:true
}
}
But updating state like this,
this.setState({ someProperty.flag: false });
doesn\'t work. How can this be done correctly?
回答1:
In order to setState
for a nested object you can follow the below approach as I think setState doesn\'t handle nested updates.
var someProperty = {...this.state.someProperty}
someProperty.flag = true;
this.setState({someProperty})
The idea is to create a dummy object perform operations on it and then replace the component\'s state with the updated object
Now, the spread operator creates only one level nested copy of the object. If your state is highly nested like:
this.state = {
someProperty: {
someOtherProperty: {
anotherProperty: {
flag: true
}
..
}
...
}
...
}
You could setState using spread operator at each level like
this.setState(prevState => ({
...prevState,
someProperty: {
...prevState.someProperty,
someOtherProperty: {
...prevState.someProperty.someOtherProperty,
anotherProperty: {
...prevState.someProperty.someOtherProperty.anotherProperty,
flag: false
}
}
}
}))
However the above syntax get every ugly as the state becomes more and more nested and hence I recommend you to use immutability-helper
package to update the state.
See this answer on how to update state with immutability helper
.
回答2:
To write it in one line
this.setState({ someProperty: { ...this.state.someProperty, flag: false} });
回答3:
Sometimes direct answers are not the best ones :)
Short version:
this code
this.state = {
someProperty: {
flag: true
}
}
should be simplified as something like
this.state = {
somePropertyFlag: true
}
Long version:
Currently you shouldn\'t want to work with nested state in React. Because React is not oriented to work with nested states and all solutions proposed here look as hacks. They don\'t use the framework but fight with it. They suggest to write not so clear code for doubtful purpose of grouping some properties. So they are very interesting as an answer to the challenge but practically useless.
Lets imagine the following state:
{
parent: {
child1: \'value 1\',
child2: \'value 2\',
...
child100: \'value 100\'
}
}
What will happen if you change just a value of child1
? React will not re-render the view because it uses shallow comparison and it will find that parent
property didn\'t change. BTW mutating the state object directly is considered to be a bad practice in general.
So you need to re-create the whole parent
object. But in this case we will meet another problem. React will think that all children have changed their values and will re-render all of them. Of course it is not good for performance.
It is still possible to solve that problem by writing some complicated logic in shouldComponentUpdate()
but I would prefer to stop here and use simple solution from the short version.
回答4:
If you are using ES2015 you have access to the Object.assign. You can use it as follows to update a nested object.
this.setState({
someProperty: Object.assign({}, this.state.someProperty, {flag: false})
});
You merge the updated properties with the existing and use the returned object to update the state.
Edit: Added an empty object as target to the assign function to make sure the state isn\'t mutated directly as carkod pointed out.
回答5:
Disclaimer
Nested State in React is wrong design
Read this excellent answer.
Reasoning behind this answer:
React\'s setState is just a built-in convenience, but you soon realise
that it has its limits. Using custom properties and intelligent use of
forceUpdate gives you much more.
eg:
class MyClass extends React.Component {
myState = someObject
inputValue = 42
...
MobX, for example, ditches state completely and uses custom observable properties.
Use Observables instead of state in React components.
the best answer of all - see example here
There is another shorter way to update whatever nested property.
this.setState(state => {
state.nested.flag = false
state.another.deep.prop = true
return state
})
On one line
this.setState(state => (state.nested.flag = false, state))
Of course this is abusing some core principles, as the state
should be read-only, but since you are immediately discarding the old state and replacing it with new state, it is completely ok.
Warning
Even though the component containing the state will update and rerender properly (except this gotcha), the props will fail to propagate to children (see Spymaster\'s comment below). Only use this technique if you know what you are doing.
For example, you may pass a changed flat prop that is updated and passed easily.
render(
//some complex render with your nested state
<ChildComponent complexNestedProp={this.state.nested} pleaseRerender={Math.random()}/>
)
Now even though reference for complexNestedProp did not change (shouldComponentUpdate)
this.props.complexNestedProp === nextProps.complexNestedProp
the component will rerender whenever parent component updates, which is the case after calling this.setState
or this.forceUpdate
in the parent.
回答6:
There are many libraries to help with this. For example, using immutability-helper:
import update from \'immutability-helper\';
const newState = update(this.state, {
someProperty: {flag: {$set: false}},
};
this.setState(newState);
Or using lodash/fp:
import {merge} from \'lodash/fp\';
const newState = merge(this.state, {
someProperty: {flag: false},
});
回答7:
Here\'s a variation on the first answer given in this thread which doesn\'t require any extra packages, libraries or special functions.
state = {
someProperty: {
flag: \'string\'
}
}
handleChange = (value) => {
const newState = {...this.state.someProperty, flag: value}
this.setState({ someProperty: newState })
}
In order to set the state of a specific nested field, you have set the whole object. I did this by creating a variable, newState
and spreading the contents of the current state into it first using the ES2015 spread operator. Then, I replaced the value of this.state.flag
with the new value (since I set flag: value
after I spread the current state into the object, the flag
field in the current state is overridden). Then, I simply set the state of someProperty
to my newState
object.
回答8:
You can also go this way (which feels more readable to me):
const newState = Object.assign({}, this.state);
newState.property.nestedProperty = \'newValue\';
this.setState(newState);
回答9:
We use Immer https://github.com/mweststrate/immer to handle these kinds of issues.
Just replaced this code in one of our components
this.setState(prevState => ({
...prevState,
preferences: {
...prevState.preferences,
[key]: newValue
}
}));
With this
import produce from \'immer\';
this.setState(produce(draft => {
draft.preferences[key] = newValue;
}));
With immer you handle your state as a \"normal object\".
The magic happens behind the scene with proxy objects.
回答10:
I used this solution.
If you have a nested state like this:
this.state = {
formInputs:{
friendName:{
value:\'\',
isValid:false,
errorMsg:\'\'
},
friendEmail:{
value:\'\',
isValid:false,
errorMsg:\'\'
}
}
you can declare the handleChange function that copy current status and re-assigns it with changed values
handleChange(el) {
let inputName = el.target.name;
let inputValue = el.target.value;
let statusCopy = Object.assign({}, this.state);
statusCopy.formInputs[inputName].value = inputValue;
this.setState(statusCopy);
}
here the html with the event listener
<input type=\"text\" onChange={this.handleChange} \" name=\"friendName\" />
回答11:
Two other options not mentioned yet:
- If you have deeply nested state, consider if you can restructure the child objects to sit at the root. This makes the data easier to update.
- There are many handy libraries available for handling immutable state listed in the Redux docs. I recommend Immer since it allows you to write code in a mutative manner but handles the necessary cloning behind the scenes. It also freezes the resulting object so you can\'t accidentally mutate it later.
回答12:
Create a copy of the state:
let someProperty = JSON.parse(JSON.stringify(this.state.someProperty))
make changes in this object:
someProperty.flag = \"false\"
now update the state
this.setState({someProperty})
回答13:
I found this to work for me, having a project form in my case where for example you have an id, and a name and I\'d rather maintain state for a nested project.
return (
<div>
<h2>Project Details</h2>
<form>
<Input label=\"ID\" group type=\"number\" value={this.state.project.id} onChange={(event) => this.setState({ project: {...this.state.project, id: event.target.value}})} />
<Input label=\"Name\" group type=\"text\" value={this.state.project.name} onChange={(event) => this.setState({ project: {...this.state.project, name: event.target.value}})} />
</form>
</div>
)
Let me know!
回答14:
To make things generic, I worked on @ShubhamKhatri\'s and @Qwerty\'s answers.
state object
this.state = {
name: \'\',
grandParent: {
parent1: {
child: \'\'
},
parent2: {
child: \'\'
}
}
};
input controls
<input
value={this.state.name}
onChange={this.updateState}
type=\"text\"
name=\"name\"
/>
<input
value={this.state.grandParent.parent1.child}
onChange={this.updateState}
type=\"text\"
name=\"grandParent.parent1.child\"
/>
<input
value={this.state.grandParent.parent2.child}
onChange={this.updateState}
type=\"text\"
name=\"grandParent.parent2.child\"
/>
updateState method
setState as @ShubhamKhatri\'s answer
updateState(event) {
const path = event.target.name.split(\'.\');
const depth = path.length;
const oldstate = this.state;
const newstate = { ...oldstate };
let newStateLevel = newstate;
let oldStateLevel = oldstate;
for (let i = 0; i < depth; i += 1) {
if (i === depth - 1) {
newStateLevel[path[i]] = event.target.value;
} else {
newStateLevel[path[i]] = { ...oldStateLevel[path[i]] };
oldStateLevel = oldStateLevel[path[i]];
newStateLevel = newStateLevel[path[i]];
}
}
this.setState(newstate);
}
setState as @Qwerty\'s answer
updateState(event) {
const path = event.target.name.split(\'.\');
const depth = path.length;
const state = { ...this.state };
let ref = state;
for (let i = 0; i < depth; i += 1) {
if (i === depth - 1) {
ref[path[i]] = event.target.value;
} else {
ref = ref[path[i]];
}
}
this.setState(state);
}
Note: These above methods won\'t work for arrays
回答15:
Something like this might suffice,
const isObject = (thing) => {
if(thing &&
typeof thing === \'object\' &&
typeof thing !== null
&& !(Array.isArray(thing))
){
return true;
}
return false;
}
/*
Call with an array containing the path to the property you want to access
And the current component/redux state.
For example if we want to update `hello` within the following obj
const obj = {
somePrimitive:false,
someNestedObj:{
hello:1
}
}
we would do :
//clone the object
const cloned = clone([\'someNestedObj\',\'hello\'],obj)
//Set the new value
cloned.someNestedObj.hello = 5;
*/
const clone = (arr, state) => {
let clonedObj = {...state}
const originalObj = clonedObj;
arr.forEach(property => {
if(!(property in clonedObj)){
throw new Error(\'State missing property\')
}
if(isObject(clonedObj[property])){
clonedObj[property] = {...originalObj[property]};
clonedObj = clonedObj[property];
}
})
return originalObj;
}
const nestedObj = {
someProperty:true,
someNestedObj:{
someOtherProperty:true
}
}
const clonedObj = clone([\'someProperty\'], nestedObj);
console.log(clonedObj === nestedObj) //returns false
console.log(clonedObj.someProperty === nestedObj.someProperty) //returns true
console.log(clonedObj.someNestedObj === nestedObj.someNestedObj) //returns true
console.log()
const clonedObj2 = clone([\'someProperty\',\'someNestedObj\',\'someOtherProperty\'], nestedObj);
console.log(clonedObj2 === nestedObj) // returns false
console.log(clonedObj2.someNestedObj === nestedObj.someNestedObj) //returns false
//returns true (doesn\'t attempt to clone because its primitive type)
console.log(clonedObj2.someNestedObj.someOtherProperty === nestedObj.someNestedObj.someOtherProperty)
回答16:
I take very seriously the concerns already voiced around creating a complete copy of your component state. That being said, if compromised performance is not of paramount concern (i.e. you don\'t have a very deep tree of components which React would want to re-render) I would suggest Immer.
import produce from \'immer\';
<Input
value={this.state.form.username}
onChange={e => produce(this.state, s => { s.form.username = e.target.value }) } />
This should work for React.PureComponent
(i.e. shallow state comparisons by React) as Immer
cleverly uses a proxy object to efficiently copy an arbitrarily deep state tree. Immer is also more typesafe compared to libraries like Immutability Helper, and is ideal for Javascript and Typescript users alike.
Typescript utility function
function setStateDeep<S>(comp: React.Component<any, S, any>, fn: (s:
Draft<Readonly<S>>) => any) {
comp.setState(produce(comp.state, s => { fn(s); }))
}
onChange={e => setStateDeep(this, s => s.form.username = e.target.value)}