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


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

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

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

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

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

14 96

14 ответов:

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

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

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

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

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

вы можете установить состояние с помощью оператора 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 свойство не изменилось. Кстати мутируя состояние объект непосредственно считается плохой практикой в целом.

так что вам нужно воссоздать все

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

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

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

в одну строку

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

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

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

даже если компонент, содержащий состояние будет обновление и rerender правильно (кроме этого Гоча), то реквизит будет fail размножать детей (см. комментарий 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 С новым значением (так как я набор 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 docs. Я рекомендую Иммер, поскольку он позволяет писать код в мутационной форме, но регулирует необходимые клонирование за кадром. Он также замораживает полученный результат объект, чтобы вы не могли случайно мутировать его позже.

чтобы сделать вещи общими, я работал над ответами @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 как ответ @Qwerty

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

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

мы используем Immer 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)