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

147

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

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

Но обновляя состояние, подобное этому,

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

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

  • 2
    Что значит не работает? Это не очень хороший вопрос - что случилось? Есть ошибки? Какие ошибки?
  • 0
    Попробуйте прочитать: stackoverflow.com/questions/18933985/…
Показать ещё 3 комментария
Теги:
setstate

16 ответов

191
Лучший ответ

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

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

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

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

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

Вы можете установитьState с помощью оператора спредов на каждом уровне, например

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

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

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

  • 0
    Разве это не имеет прямого доступа к государству и, следовательно, нарушает идею «состояний» в целом?
  • 2
    @ Ohgodwhy, нет, это не имеет прямого доступа к состоянию, так как {... this.state.someProperty} возвращает новый объект в someProperty и мы someProperty это
Показать ещё 9 комментариев
78

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

this.setState({ someProperty: { ...this.state.someProperty, flag: false} });
  • 9
    Рекомендуется использовать средство обновления для setState вместо прямого доступа к this.state в setState. reactjs.org/docs/react-component.html#setstate
  • 3
    Я считаю, что это говорит, не читайте this.state сразу после setState и ожидайте, что он будет иметь новое значение. По сравнению с вызовом this.state в setState . Или есть проблема с последним, которая мне не ясна?
Показать ещё 1 комментарий
34

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

Укороченная версия:

этот код

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

Поэтому вам нужно воссоздать весь parent объект. Но в этом случае мы встретим еще одну проблему. Реакция будет думать, что все дети изменили свои ценности и будут повторно отображать их все. Конечно, это плохо для производительности.

По-прежнему можно решить эту проблему, написав сложную логику в shouldComponentUpdate() но я предпочел бы остановиться здесь и использовать простое решение из короткой версии.

  • 1
    Я думаю, что есть случаи, когда вложенное состояние прекрасно. Например, состояние, динамически отслеживающее пары ключ-значение. Посмотрите здесь, как вы можете обновить состояние только для одной записи в этом состоянии: Reactionjs.org/docs/…
  • 0
    Мелкое сравнение используется для чистых компонентов по умолчанию.
Показать ещё 5 комментариев
24

отказ

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

Прочитайте этот отличный ответ.

Причины этого ответа:

React setState - это просто встроенное удобство, но вы скоро поймете, что оно имеет свои пределы. Использование пользовательских свойств и интеллектуальное использование forceUpdate дает вам гораздо больше. например:

class MyClass extends React.Component {
    myState = someObject
    inputValue = 42
...

Например, MobX полностью скрывает состояние канав и использует настраиваемые наблюдаемые свойства.
Используйте Observables вместо состояния в компонентах React.


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

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

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

На одной линии

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

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

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

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

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

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

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

this.props.complexNestedProp === nextProps.complexNestedProp

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

  • 8
    Вы не должны нигде мутировать любое реагирующее состояние. Если есть вложенные компоненты, которые используют данные из state.nested или state.another, они не будут повторно отображаться, как и их дочерние элементы. Это потому, что объект не изменился, поэтому React считает, что ничего не изменилось.
  • 0
    @ SpyMaster356 Я обновил свой ответ, чтобы решить эту проблему. Также обратите внимание, что, например, MobX полностью устанавливает state канав и использует настраиваемые наблюдаемые свойства. React setState - это просто встроенное удобство, но вы скоро поймете, что у него есть свои пределы. Использование пользовательских свойств и интеллектуальное использование forceUpdate дает вам гораздо больше. Используйте Observables вместо состояния в компонентах React.
Показать ещё 3 комментария
19

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

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

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

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

  • 10
    И я могу ошибаться, но я не думаю, что вы используете React без ES2015.
7

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

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},
});
  • 0
    Если вы используете lodash , вы, вероятно, захотите попробовать _.cloneDeep и установить состояние в качестве клонированной копии.
  • 0
    @YixingLiu: я обновил ответ, чтобы использовать lodash/fp , поэтому нам не нужно беспокоиться о мутациях.
5

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

const newState = Object.assign({}, this.state);
newState.property.nestedProperty = 'newValue';
this.setState(newState);
  • 0
    кажется более элегантным решением здесь, 1) избегая утомительного распространения внутри setState и 2) красиво вставляя новое состояние в setState, избегая мутировать непосредственно внутри setState. И последнее, но не менее важное: 3) избегать использования какой-либо библиотеки для простой задачи
5

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

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 состояние someProperty для моего объекта newState.

3

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

2

Два других варианта, которые еще не упомянуты:

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

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

Если у вас есть вложенное состояние:

   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

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

import produce from 'immer';

<Input
  value={this.state.form.username}
  onChange={e => produce(this.state, s => { s.form.username = e.target.value }) } />

Это должно работать для React.PureComponent (т. React.PureComponent сравнение состояний с помощью React), поскольку Immer ловко использует прокси-объект для эффективного копирования произвольно глубокого дерева состояний. Immer также более безопасен по сравнению с такими библиотеками, как Immutability Helper, и идеально подходит для пользователей Javascript и Typescript.


Полезность Typescript

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

Создайте копию состояния:
let someProperty = JSON.parse(JSON.stringify(this.state.someProperty))

внести изменения в этот объект:
someProperty.flag = "false"

сейчас обновите состояние
this.setState({someProperty})

0

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

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

Чтобы сделать вещи родовыми, я работал над ответами @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);
}

Примечание. Эти методы не будут работать для массивов

0

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

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

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

Ещё вопросы

Сообщество Overcoder
Наверх
Меню