Подтвердить что ты не робот

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

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

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

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

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

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

4b9b3361

Ответ 1

Чтобы 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.

Ответ 2

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

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

Ответ 3

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

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

этот код

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() но я предпочел бы остановиться здесь и использовать простое решение из короткой версии.

Ответ 4

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

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

Read this excellent answer.

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

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

Это фактически то же самое, что и

this.state.nested.flag = false
this.forceUpdate()

Тонкую разницу в этом контексте между forceUpdate и setState смотрите в связанном примере.

Конечно, это нарушает некоторые основные принципы, так как 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 в родительском.

Эффекты мутирования государства

Использование вложенного состояния и непосредственное изменение состояния опасно, поскольку разные объекты могут содержать (намеренно или нет) разные (более старые) ссылки на состояние и могут не обязательно знать, когда обновлять (для Например, когда используется PureComponent или shouldComponentUpdate реализован для возврата false) ИЛИ предназначены для отображения старых данных, как в примере ниже.

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

state-flow state-flow-nested

Во всяком случае, здесь вы можете видеть, что Nested PureChildClass он не рендерился из-за того, что реквизит не распространяется.

Ответ 5

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

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

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

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

Ответ 6

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

import update from 'immutability-helper';

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

Используя набор lodash/fp:

import {set} from 'lodash/fp';

const newState = set(["someProperty", "flag"], false, this.state);

Использование lodash/fp merge:

import {merge} from 'lodash/fp';

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

Ответ 7

const newState = Object.assign({}, this.state);
newState.property.nestedProperty = "new value";
this.setState(newState);

Ответ 8

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

Ответ 9

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

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.

Ответ 10

Хотя на самом деле вложение - это не то, как следует обращаться с состоянием компонента, иногда для чего-то более простого для одноуровневого вложения.

Для такого государства

state = {
 contact: {
  phone: '888-888-8888',
  email: '[email protected]'
 }
 address: {
  street:''
 },
 occupation: {
 }
}

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

handleChange = (obj) => e => {
  let x = this.state[obj];
  x[e.target.name] = e.target.value;
  this.setState({ [obj]: x });
};

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

<TextField
 name="street"
 onChange={handleChange('address')}
 />

Ответ 11

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

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

   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" />

Ответ 12

Создайте копию состояния:

let someProperty = JSON.parse(JSON.stringify(this.state.someProperty))

внести изменения в этот объект:

someProperty.flag = "false"

теперь обновите состояние

this.setState({someProperty})

Ответ 13

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

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

Ответ 14

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

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

Ответ 15

Я очень серьезно отношусь к проблемам, уже высказанным в связи с созданием полной копии состояния вашего компонента. С учетом сказанного, я настоятельно рекомендую 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)}

Ответ 16

stateUpdate = () => {
    let obj = this.state;
    if(this.props.v12_data.values.email) {
      obj.obj_v12.Customer.EmailAddress = this.props.v12_data.values.email
    }
    this.setState(obj)
}

Ответ 17

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

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

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

Ответ 18

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

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) 

Ответ 19

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

  constructor(props) {
    super(props);

    this.state = {
      loading: false,
      user: {
        email: ""
      },
      organization: {
        name: ""
      }
    };

    this.handleChange = this.handleChange.bind(this);
  }

Моя функция handleChange выглядит так:

  handleChange(e) {
    const names = e.target.name.split(".");
    const value = e.target.type === "checkbox" ? e.target.checked : e.target.value;
    this.setState((state) => {
      state[names[0]][names[1]] = value;
      return {[names[0]]: state[names[0]]};
    });
  }

И убедитесь, что вы называете входные данные соответственно:

<input
   type="text"
   name="user.email"
   onChange={this.handleChange}
   value={this.state.user.firstName}
   placeholder="Email Address"
/>

<input
   type="text"
   name="organization.name"
   onChange={this.handleChange}
   value={this.state.organization.name}
   placeholder="Organization Name"
/>

Ответ 20

Я делаю вложенные обновления с сокращением поиска:

Пример:

Вложенные переменные в состоянии:

state = {
    coords: {
        x: 0,
        y: 0,
        z: 0
    }
}

Функция:

handleChange = nestedAttr => event => {
  const { target: { value } } = event;
  const attrs = nestedAttr.split('.');

  let stateVar = this.state[attrs[0]];
  if(attrs.length>1)
    attrs.reduce((a,b,index,arr)=>{
      if(index==arr.length-1)
        a[b] = value;
      else if(a[b]!=null)
        return a[b]
      else
        return a;
    },stateVar);
  else
    stateVar = value;

  this.setState({[attrs[0]]: stateVar})
}

Использование:

<input
value={this.state.coords.x}
onChange={this.handleTextChange('coords.x')}
/>