Реагируйте: как обновить state.item [1] в состоянии, используя setState?

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

Компонент доступен как JSFiddle здесь.

Мое начальное состояние выглядит так:

var DynamicForm = React.createClass({
  getInitialState: function() {
   var items = {};
   items[1] = { name: 'field 1', populate_at: 'web_start',
                same_as: 'customer_name',
                autocomplete_from: 'customer_name', title: '' };
   items[2] = { name: 'field 2', populate_at: 'web_end',
                same_as: 'user_name', 
                    autocomplete_from: 'user_name', title: '' };

     return { items };
   },

  render: function() {
     var _this = this;
     return (
       <div>
         { Object.keys(this.state.items).map(function (key) {
           var item = _this.state.items[key];
           return (
             <div>
               <PopulateAtCheckboxes this={this}
                 checked={item.populate_at} id={key} 
                   populate_at={data.populate_at} />
            </div>
            );
        }, this)}
        <button onClick={this.newFieldEntry}>Create a new field</button>
        <button onClick={this.saveAndContinue}>Save and Continue</button>
      </div>
    );
  }

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

var PopulateAtCheckboxes = React.createClass({
  handleChange: function (e) {
     item = this.state.items[1];
     item.name = 'newName';
     items[1] = item;
     this.setState({items: items});
  },
  render: function() {
    var populateAtCheckbox = this.props.populate_at.map(function(value) {
      return (
        <label for={value}>
          <input type="radio" name={'populate_at'+this.props.id} value={value}
            onChange={this.handleChange} checked={this.props.checked == value}
            ref="populate-at"/>
          {value}
        </label>
      );
    }, this);
    return (
      <div className="populate-at-checkboxes">
        {populateAtCheckbox}
      </div>
    );
  }
});

Как я должен создать this.setState чтобы он обновлял items[1].name?

Ответ 1

Для этого можно использовать update помощник по неизменности:

this.setState({
  items: update(this.state.items, {1: {name: {$set: 'updated field name'}}})
})

Или, если вы не можете обнаружить изменения этого элемента в методе жизненного цикла shouldComponentUpdate(), используя ===, вы можете отредактировать состояние напрямую и заставить компонент повторно отобразить - это эффективно так же, как ответ @limelights, поскольку он вытягивает объект из состояния и редактирует его.

this.state.items[1].name = 'updated field name'
this.forceUpdate()

Добавление после редактирования:

Изучите Простую Компонентную Коммуникацию из react-training для примера того, как пройти callback от родителя, содержащего состояние, к дочернему компоненту, который должен вызвать изменение состояния.

Ответ 2

Неправильный способ!

handleChange = (e) => {
    const { items } = this.state;
    items[1].name = e.target.value;

    // update state
    this.setState({
        items,
    });
};

Как отмечают многие лучшие разработчики в комментариях: мутировать состояние - это неправильно!

Мне понадобилось время, чтобы понять это. Выше работает, но он отнимает силу Реакта. Например, componentDidUpdate не увидит это как обновление, поскольку оно было изменено напрямую.

Таким образом, правильный путь будет:

handleChange = (e) => {
    this.setState(prevState => ({
        items: {
            ...prevState.items,
            [prevState.items[1].name]: e.target.value,
        },
    }));
};

Ответ 3

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

handleChange: function (e) {
    // 1. Make a shallow copy of the items
    let items = [...this.state.items];
    // 2. Make a shallow copy of the item you want to mutate
    let item = {...items[1]};
    // 3. Replace the property you're intested in
    item.name = 'newName';
    // 4. Put it back into our array. N.B. we *are* mutating the array here, but that why we made a copy first
    items[1] = item;
    // 5. Set the state to our new copy
    this.setState({items});
},

Вы можете объединить шаги 2 и 3, если хотите:

let item = {
    ...items[1],
    name: 'newName'
}

Или вы можете сделать все это в одной строке:

this.setState(({items}) => ({
    items: [
        ...items.slice(0,1),
        {
            ...items[1],
            name: 'newName',
        },
        ...items.slice(2)
    ]
}));

Примечание: я сделал items массивом. ОП использовал объект. Однако понятия одинаковы.


Вы можете увидеть, что происходит в вашем терминале/консоли:

❯ node
> items = [{name:'foo'},{name:'bar'},{name:'baz'}]
[ { name: 'foo' }, { name: 'bar' }, { name: 'baz' } ]
> clone = [...items]
[ { name: 'foo' }, { name: 'bar' }, { name: 'baz' } ]
> item1 = {...clone[1]}
{ name: 'bar' }
> item1.name = 'bacon'
'bacon'
> clone[1] = item1
{ name: 'bacon' }
> clone
[ { name: 'foo' }, { name: 'bacon' }, { name: 'baz' } ]
> items
[ { name: 'foo' }, { name: 'bar' }, { name: 'baz' } ] // good! we didn't mutate 'items'
> items === clone
false // these are different objects
> items[0] === clone[0]
true // we don't need to clone items 0 and 2 because we're not mutating them (efficiency gains!)
> items[1] === clone[1]
false // this guy we copied

Ответ 4

Для изменения глубоко вложенных объектов/переменных в состоянии React обычно используются три метода: vanilla JS Object.assign, immutability-helper и cloneDeep из Lodash. Для этого есть множество других менее популярных сторонних библиотек, но в этом ответе я расскажу только об этих трех вариантах. Кроме того, есть некоторые методы, которые используют не что иное, как ванильный JavaScript, например распространение массива (см., Например, ответ @mpen), но они не очень интуитивны, просты в использовании и способны обрабатывать все ситуации с манипуляциями с состоянием.

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

Давайте сравним три широко используемых метода.

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

Учитывая эту государственную структуру:

state = {
    foo: {
        bar: 'initial value'
    }
}

1. Vanilla JavaScript Object.assign

(...essential imports)
class App extends Component {

    state = {
        foo: {
            bar: 'initial value'
        }
    }

    componentDidMount() {

        console.log(this.state.foo.bar) // initial value

        const foo = Object.assign({}, this.state.foo, { bar: 'further value' })

        console.log(this.state.foo.bar) // initial value

        this.setState({ foo }, () => {
            console.log(this.state.foo.bar) // further value
        })
    }
    (...rest of code)

Помните, что Object.assign небудет выполнять глубокое клонирование, поскольку он только копирует значения свойств, и поэтому то, что он делает, называется поверхностным копирование (см. комментарии).

Чтобы это работало, нам следует манипулировать только объектами верхнего уровня (state.foo). И их значения (state.foo.bar) должны быть примитивными (строки, числа, логические значения).

В этом примере мы создаем новую константу (const foo...), используя Object.assign, которая создает пустой объект ({}), копирует в него объект state.foo ({ bar: 'initial value' }) и затем копирует другую объект { bar: 'further value' } над ним. Итак, в конце концов, вновь созданная константа foo будет содержать значение { bar: 'further value' }, так как свойство bar было переопределено. Этот foo является совершенно новым объектом, который не связан с объектом состояния, поэтому его можно изменять по мере необходимости, и состояние не изменится.

Последняя часть заключается в использовании установщика setState() для замены исходного состояния state.foo в состоянии на вновь созданный объект foo.

Теперь представьте, что у нас есть более глубокое состояние, подобное state = { foo: { bar: { baz: 'initial value' } } }. Мы могли бы попытаться создать новый объект foo и заполнить его содержимым foo из состояния, но Object.assign не сможет скопировать значение baz в этот вновь созданный объект foo, поскольку baz вложено слишком глубоко. Вы все еще можете скопировать bar, как в примере выше, но, поскольку это объект сейчас, а не примитив, вместо этого будет скопирована ссылка из state.foo.bar, что означает, что мы получим непосредственно локальный объект foo привязан к государству. Это означает, что в этом случае любые мутации локально созданного foo будут влиять на объект state.foo, поскольку они фактически указывают на одно и то же.

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

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

2. Lodash cloneDeep

(...essential imports)
import cloneDeep from 'lodash.clonedeep'

class App extends Component {

    state = {
        foo: {
            bar: 'initial value'
        }
    }

    componentDidMount() {

        console.log(this.state.foo.bar) // initial value

        const foo = cloneDeep(this.state.foo)

        foo.bar = 'further value'  

        console.log(this.state.foo.bar) // initial value

        this.setState({ foo }, () => {
            console.log(this.state.foo.bar) // further value
        })
    }
    (...rest of code)

Lodash cloneDeep гораздо проще в использовании. Он выполняет глубокое клонирование, поэтому это надежный вариант, если у вас достаточно сложное состояние с многоуровневыми объектами или массивами внутри. Просто cloneDeep() свойство состояния верхнего уровня, измените клонированную часть, как вам угодно, и setState() верните ее в состояние.

3. помощник по неизменяемости

(...essential imports)
import update from 'immutability-helper'

class App extends Component {

    state = {
        foo: {
            bar: 'initial value'
        }
    };

    componentDidMount() {

        console.log(this.state.foo.bar) // initial value     

        const foo = update(this.state.foo, { bar: { $set: 'further value' } })  

        console.log(this.state.foo.bar) // initial value

        this.setState({ foo }, () => {
            console.log(this.state.foo.bar) // further value
        });
    }    
    (...rest of code)

immutability-helper выводит его на совершенно новый уровень, и самое интересное в этом то, что он может не только $set значениями указывать элементы, но также $push, $splice, $merge (и т.д.). ) им. Вот список доступных команд.

Дополнительные примечания

Опять же, имейте в виду, что this.setState() изменяет только свойства первого уровня объекта состояния (в данном случае свойство foo), но не глубоко вложенный (foo.bar). Если бы он вел себя по-другому, этот вопрос не существовал бы.

И, между прочим, this.setState({ foo }) это просто сокращение для this.setState({ foo: foo }). И () => { console.log(this.state.foo.bar) } сразу после { foo } является обратным вызовом, который выполняется сразу после того, как setState установил состояние. Удобно, если вам нужно сделать что-то после того, как оно выполнит свою работу (в нашем случае, чтобы отобразить состояние сразу после того, как оно было установлено).

Какой из них подходит для вашего проекта?

Если вы не хотите или можете использовать внешние зависимости и иметь простую структуру состояний, придерживайтесь Object.assign.

Если вы манипулируете огромным и/или сложным состоянием, Lodash cloneDeep - мудрый выбор.

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

... или вам вообще нужно это делать?

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

Скорее всего, вам лучше не хранить свои сложные данные в хранилище Redux, устанавливать их там с помощью редукторов и/или саг и обращаться к ним с помощью селекторов.

Ответ 5

У меня такая же проблема. Вот простое решение, которое работает!

const newItems = [...this.state.items];
newItems[item] = value;
this.setState({ items:newItems });

Ответ 6

Сначала получите нужный элемент, измените то, что вы хотите на этом объекте, и верните его в состояние. То, как вы используете состояние, только передавая объект в getInitialState, было бы проще, если бы вы использовали ключевой объект.

handleChange: function (e) {
   item = this.state.items[1];
   item.name = 'newName';
   items[1] = item;

   this.setState({items: items});
}

Ответ 7

Согласно документации React для setState, использование Object.assign как предлагается другими ответами, здесь не идеально. Из-за характера асинхронного поведения setState последующие вызовы, использующие эту технику, могут переопределять предыдущие вызовы, вызывая нежелательные результаты.

Вместо этого в документах React рекомендуется использовать форму обновления setState которая работает в предыдущем состоянии. Имейте в виду, что при обновлении массива или объекта вы должны возвращать новый массив или объект, поскольку React требует от нас сохранения неизменности состояния. Использование оператора расширения синтаксиса ES6 для поверхностного копирования массива, создание или обновление свойства объекта по заданному индексу массива будет выглядеть так:

this.setState(prevState => {
    const newItems = [...prevState.items];
    newItems[index].name = newName;
    return {items: newItems};
})

Ответ 8

Не мутируйте состояние на месте. Это может привести к неожиданным результатам. Я узнал мой урок! Всегда работайте с копией/клоном, Object.assign() является хорошим:

item = Object.assign({}, this.state.items[1], {name: 'newName'});
items[1] = item;
this.setState({items: items});

https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/assign

Ответ 9

EDIT:

Для глубокого клона:

const groups = JSON.parse(JSON.stringify(this.state.groups[index]))

пример реального мира с ES6:

handleChange(index, e) {
   const groups = [...this.state.groups];
   groups[index].name = e.target.value;
   this.setState({ groups });
}

Ответ 10

Это действительно просто.

Сначала вытащите весь объект items из состояния, обновите часть объекта items по своему усмотрению и верните весь объект items обратно в состояние через setState.

handleChange: function (e) {
  items = Object.assign(this.state.items); // Pull the entire items object out. Using object.assign is a good idea for objects.
  items[1].name = 'newName'; // update the items object as needed
  this.setState({ items }); // Put back in state
}

Ответ 11

Без мутации:

// given a state
state = {items: [{name: 'Fred', value: 1}, {name: 'Wilma', value: 2}]}

// This will work without mutation as it clones the modified item in the map:
this.state.items
   .map(item => item.name === 'Fred' ? {...item, ...{value: 3}} : item)

this.setState(newItems)

Ответ 12

Используйте событие на handleChange, чтобы выяснить элемент, который был изменен, а затем обновить его. Для этого вам может потребоваться изменить какое-либо свойство, чтобы идентифицировать его и обновить.

Смотрите скрипку https://jsfiddle.net/69z2wepo/6164/

Ответ 13

Как насчет создания другого компонента (для объекта, который должен войти в массив) и передачи следующего в качестве реквизита?

  1. индекс компонента - индекс будет использоваться для создания/обновления в массиве.
  2. Функция set - эта функция помещает данные в массив на основе индекса компонента.
<SubObjectForm setData={this.setSubObjectData}                                                            objectIndex={index}/>

Здесь {index} может быть передан в зависимости от позиции, где используется этот SubObjectForm.

и setSubObjectData может быть что-то вроде этого.

 setSubObjectData: function(index, data){
      var arrayFromParentObject= <retrieve from props or state>;
      var objectInArray= arrayFromParentObject.array[index];
      arrayFromParentObject.array[index] = Object.assign(objectInArray, data);
 }

В SubObjectForm this.props.setData может вызываться при изменении данных, как указано ниже.

<input type="text" name="name" onChange={(e) => this.props.setData(this.props.objectIndex,{name: e.target.value})}/>

Ответ 14

Я бы переместил изменение дескриптора функции и добавил индексный параметр

handleChange: function (index) {
    var items = this.state.items;
    items[index].name = 'newName';
    this.setState({items: items});
},

в компонент Dynamic form и передать его компоненту PopulateAtCheckboxes в качестве опоры. Когда вы перебираете элементы, вы можете включить дополнительный счетчик (называемый индексом в приведенном ниже коде), который будет передан вместе с изменением дескриптора, как показано ниже.

{ Object.keys(this.state.items).map(function (key, index) {
var item = _this.state.items[key];
var boundHandleChange = _this.handleChange.bind(_this, index);
  return (
    <div>
        <PopulateAtCheckboxes this={this}
            checked={item.populate_at} id={key} 
            handleChange={boundHandleChange}
            populate_at={data.populate_at} />
    </div>
);
}, this)}

Наконец, вы можете вызвать своего слушателя изменений, как показано ниже.

<input type="radio" name={'populate_at'+this.props.id} value={value} onChange={this.props.handleChange} checked={this.props.checked == value} ref="populate-at"/>

Ответ 15

Если вам нужно изменить только часть Array, у вас есть компонент реагирования с состоянием, установленным в.

state = {items: [{name: 'red-one', value: 100}, {name: 'green-one', value: 999}]}

Лучше всего обновить red-one в Array следующим образом:

const itemIndex = this.state.items.findIndex(i=> i.name === 'red-one');
const newItems = [
   this.state.items.slice(0, itemIndex),
   {name: 'red-one', value: 666},
   this.state.items.slice(itemIndex)
]

this.setState(newItems)

Ответ 16

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

this.setState({items: this.state.items.map((item,idx)=> idx!==1 ?item :{...item,name:'new_name'}) })

Ответ 17

или если у вас есть динамически генерируемый список, и вы не знаете индекс, а просто имеете ключ или идентификатор:

let ItemsCopy = []
let x = this.state.Items.map((entry) =>{

    if(entry.id == 'theIDYoureLookingFor')
    {
        entry.PropertyToChange = 'NewProperty'
    }

    ItemsCopy.push(entry)
})


this.setState({Items:ItemsCopy});

Ответ 18

Попробуйте с кодом:

this.state.items[1] = 'new value';
var cloneObj = Object.assign({}, this.state.items);

this.setState({items: cloneObj });

Ответ 19

Следующий фрагмент кода легок в моем унылом мозгу. Удаление объекта и замена на обновленный

    var udpateditem = this.state.items.find(function(item) { 
                   return item.name == "field_1" });
    udpateditem.name= "New updated name"                       
    this.setState(prevState => ({                                   
    items:prevState.dl_name_template.filter(function(item) { 
                                    return item.name !== "field_1"}).concat(udpateditem)
    }));

Ответ 20

Попробуйте обновить handleChange следующим образом:

 handleChange: function (e) {
   const items = this.state.items;
   items[1].name = "newName";
   this.setState({items: items});
 },

Ответ 21

Попробуйте это, это определенно сработает, в другом случае я попытался, но не работал.

import _ from 'lodash';

this.state.var_name  = _.assign(this.state.var_name, {
   obj_prop: 'changed_value',
});