it-roy-ru.com

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

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

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

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

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

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

224
Alex Yong

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

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

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

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

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

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

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

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

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

275
Shubham Khatri

Написать в одну строку

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

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

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

этот код

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

должно быть упрощено как-то вроде

this.state = {
    somePropertyFlag: true
}

Длинная версия:

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

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

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

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

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

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

55
Konstantin Smolyanin

Отказ

Вложенное состояние в 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))

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

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-flowstate-flow-nested

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

39
Qwerty

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

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

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

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

20
Alyssa Roose

Есть много библиотек, чтобы помочь с этим. Например, используя 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 слияние:

import {merge} from 'lodash/fp';

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

С ES6:

this.setState({...this.state, property: {nestedProperty: "new value"}})

Что эквивалентно:

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

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

6
Joakim Jäderberg

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

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 в мой объект newState.

5
Matthew Berkompas

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

Если у вас есть вложенное состояние, подобное этому:

   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" />
3
Alberto Piras

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

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

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

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

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

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

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

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

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

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')}
 />
1
user2208124

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

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

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

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

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

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

0
Michael Stokes

Нечто подобное может быть достаточно,

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
Eladian