Как обновить вложенные свойства состояния в React

Я пытаюсь организовать свое состояние, используя вложенное свойство следующим образом:

this.state = {
   someProperty: {
      flag:true
   }
}

но обновление состояния, как это,

this.setState({ someProperty.flag: false });

не работает. Как это можно сделать правильно?

14 ответов


для того чтобы setState для вложенного объекта вы можете следовать приведенному ниже подходу, поскольку я думаю, что setState не обрабатывает вложенные обновления.

var someProperty = {...this.state.someProperty}
someProperty.flag = true;
this.setState({someProperty})

идея состоит в том, чтобы создать фиктивный объект, выполнить над ним операции, а затем заменить состояние компонента обновленным объектом

теперь оператор spread создает только один уровень вложенной копии объекта. Если ваше состояние сильно вложено, как:

this.state = {
   someProperty: {
      someOtherProperty: {
          anotherProperty: {
             flag: true
          }
          ..
      }
      ...
   }
   ...
}

вы можете setState с помощью оператора spread на каждом уровне как

this.setState(prevState => ({
    ...prevState,
    someProperty: {
        ...prevState.someProperty,
        someOtherProperty: {
            ...prevState.someProperty.someOtherProperty, 
            anotherProperty: {
               ...prevState.someProperty.someOtherProperty.anotherProperty,
               flag: false
            }
        }
    }
}))

однако приведенный выше синтаксис становится все уродливым, поскольку состояние становится все более вложенным, и поэтому я рекомендую вам использовать immutability-helper пакет для обновления состояния.

см. этот ответ о том, как обновить состояние с помощью immutability helper.


записать его в одну строку

this.setState({ someProperty: { ...this.state.someProperty, flag: false} });

Если вы используете ES2015 у вас есть доступ к объекту.назначить. Вы можете использовать его следующим образом для обновления вложенного объекта.

this.setState({
  someProperty: Object.assign({}, this.state.someProperty, {flag: false})
});

вы объединяете обновленные свойства с существующими и используете возвращаемый объект для обновления состояния.

Edit: добавлен пустой объект в качестве цели в функцию assign, чтобы убедиться, что состояние не мутирует напрямую, как указал carkod.


Иногда прямые ответы не самые лучшие:)

короткая версия:

код

this.state = {
    someProperty: {
        flag: true
    }
}

следует упростить как что-то вроде

this.state = {
    somePropertyFlag: true
}

версия:

в настоящее время вы не должны хотеть работать с вложенным состоянием в React. Потому что React не ориентирован на работу с вложенными состояниями, и все предлагаемые здесь решения выглядят как хаки. Они не используйте фреймворк, но боритесь с ним. Они предлагают писать не столь понятный код с сомнительной целью группировки некоторых свойств. Поэтому они очень интересны, как ответ на вызов, но практически бесполезно.

давайте представим следующее состояние:

{
    parent: {
        child1: 'value 1',
        child2: 'value 2',
        ...
        child100: 'value 100'
    }
}

что произойдет, если вы измените только значение child1? React не будет повторно отображать представление, потому что он использует неглубокое сравнение, и он найдет, что parent свойство не изменилось. BTW мутирует состояние объект непосредственно считается плохой практикой в целом.

таким образом, вам нужно воссоздать целое


лучше всего - см. пример здесь

есть еще короче способ обновления любого вложенного свойства.

this.setState(state => {
  state.nested.flag = false
  state.another.deep.prop = true
  return state
})

на одной строке

this.setState(state => (state.nested.flag = false, state))

конечно, это злоупотребление некоторыми основными принципами, как state должно быть только для чтения, но так как вы сразу отбрасываете старое состояние и заменяете его новым состоянием, оно полностью ладно.

предупреждение

даже если компонент, содержащий состояние будет обновление и rerender правильно (кроме этого Гоча), то реквизит будет не пропогандировать детям (см. комментарий Spymaster ниже). Используйте эту технику, только если вы знаете, что делаете.

например, вы можете передать измененную плоскую опору, которая обновляется и передается легко.

render(
  //some complex render with your nested state
  <ChildComponent complexNestedProp={this.state.nested} pleaseRerender={Math.random()}/>
)

теперь, хотя ссылка для complexNestedProp не изменилась (shouldComponentUpdate)

this.props.complexNestedProp === nextProps.complexNestedProp

компонент будет rerender всякий раз, когда родительский компонент обновляется, что имеет место после вызова this.setState или this.forceUpdate в Родительском.

отказ от ответственности

вложенное состояние в React-неправильный дизайн

читать это отлично ответ.


есть много библиотек, чтобы помочь с этим. Например, используя неизменность-помощника:

import update from 'immutability-helper';

const newState = update(this.state, {
  someProperty: {flag: {$set: false}},
};
this.setState(newState);

или через lodash / fp:

import {merge} from 'lodash/fp';

const newState = merge(this.state, {
  someProperty: {flag: false},
});

вот вариант первого ответа, данного в этом потоке, который не требует каких-либо дополнительных пакетов, библиотек или специальных функций.

state = {
  someProperty: {
    flag: 'string'
  }
}

handleChange = (value) => {
  const newState = {...this.state.someProperty, flag: value}
  this.setState({ someProperty: newState })
}

чтобы установить состояние конкретного вложенного поля, вы установили весь объект. Я сделал это, создав переменную,newState и распространение содержимого текущего состояния в него первый использование ES2015 распространение оператор. Затем Я заменил значение this.state.flag С новым значением (так как я set flag: value после я распространяю текущее состояние на объект,flag поле в текущем состоянии переопределено). Затем я просто устанавливаю состояние someProperty мой


вы также можете пойти этим путем (который кажется мне более читаемым):

const newState = Object.assign({}, this.state);
newState.property.nestedProperty = 'newValue';
this.setState(newState);

я использовал это решение.

Если у вас есть вложенный такой:

   this.state = {
          formInputs:{
            friendName:{
              value:'',
              isValid:false,
              errorMsg:''
            },
            friendEmail:{
              value:'',
              isValid:false,
              errorMsg:''
            }
}

вы можете объявить функцию handleChange, которая копирует текущий статус и повторно назначает его с измененными значениями

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

здесь html с прослушивателем событий

<input type="text" onChange={this.handleChange} " name="friendName" />

два других варианта еще не упомянул:

  1. Если у вас глубоко вложенное состояние, подумайте, Можно ли реструктурировать дочерние объекты, чтобы сидеть в корне. Это упрощает обновление данных.
  2. есть много удобных библиотек, доступных для обработки неизменяемом состоянии перечислены в документах Redux. Я рекомендую Иммер, поскольку он позволяет писать код в мутационной форме, но регулирует необходимые клонирование за кадром. Он также замораживает полученный результат объект, чтобы вы не могли случайно мутировать его позже.

чтобы сделать вещи общими, я работал над ответами @ShubhamKhatri и @Qwerty.

состояние объекта

this.state = {
  name: '',
  grandParent: {
    parent1: {
      child: ''
    },
    parent2: {
      child: ''
    }
  }
};

управление вводом

<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

setState как ответ @ShubhamKhatri в

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 как ответ @Йцукена

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

Примечание: указанные выше методы не будут работать для массивов


мы используем Иммер https://github.com/mweststrate/immer чтобы справиться с эти виды вопросов.

просто заменил этот код в одном из наших компонентов

this.setState(prevState => ({
   ...prevState,
        preferences: {
            ...prevState.preferences,
            [key]: newValue
        }
}));

С

import produce from 'immer';

this.setState(produce(draft => {
    draft.preferences[key] = newValue;
}));

С immer вы обрабатываете свое состояние как "нормальный объект". Магия происходит за кулисами с прокси-объектами.


Я нашел, что это работает для меня, имея форму проекта в моем случае, где, например, у вас есть идентификатор и имя, и я бы предпочел поддерживать состояние для вложенного проекта.

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

Дайте мне знать!


что-то вроде этого может быть достаточно,

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)