How to update nested state properties in React

2018-12-31 16:31发布

问题:

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:

  1. 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.
  2. 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)}