Реагируйте: Как я могу обновить state.item [1] на setState? (с JSFiddle)

139

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

Компонент доступен как 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?

  • 0
    Там нет никаких признаков того, как PopulateAtCheckboxes компонент в отредактированном вопрос связан с DynamicForm компонентом , который держит состояние вы пытаетесь изменить.
  • 0
    @insin, пожалуйста, смотрите мой обновленный код.
Показать ещё 1 комментарий
Теги:

16 ответов

76

Для этого можно использовать 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 от родителя, содержащего состояние, к дочернему компоненту, который должен вызвать изменение состояния.

  • 3
    Привет, с помощью update возникла эта ошибка: Uncaught ReferenceError: update is not defined
  • 0
    Не берите в голову, я добавил аддоны React и использовал React.addons.update
Показать ещё 13 комментариев
63

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

С ES6:

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

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

Обновить!

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

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

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

handleChange = (e) => {
    this.setState(prevState => ({
        items: {
            ...prevState.items,
            [item.role]: e.target.value
        },
    }));
};
  • 23
    ES6 только потому, что вы используете "const"?
  • 0
    Интересно, что это работает, вы можете немного объяснить? Спасибо!
Показать ещё 14 комментариев
43

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

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
  • 0
    Определенно стоит упомянуть, но не очень интуитивно понятен в использовании, если честно! +1
  • 2
    @TranslucentCloud Ах да, вспомогательные методы определенно хороши, но я думаю, что каждый должен знать, что происходит под капотом :-)
Показать ещё 3 комментария
32

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

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

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

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

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

1. Ванильный 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.bar), а их значения могут быть либо примитивами (строки, числа и т.д.), Либо ссылками на другие объекты (объекты, массивы).

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

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

2. Lodash cloneDeep

(...essential imports)
import { cloneDeep } from 'lodash';

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 выполняет глубокое клонирование, поэтому это надежный вариант, если у вас достаточно сложное состояние с многоуровневыми объектами или массивами.

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

Спектакль

Теперь, когда охвачены все три основных метода, давайте сравним их скорость. Я создал jsperf для этого. В этом тесте огромным состоянием с 10000 свойствами со случайными значениями манипулируют все три вышеупомянутых метода.

Вот результаты для моего Chrome 61.0.3163 на Windows 10:

+---------------------+---------+-----------+-------------+
|        Test         | Ops/sec | Precision | Explanation |
+---------------------+---------+-----------+-------------+
| Object.assign       |     272 | ±0.62%    | 56% slower  |
| cloneDeep           |     618 | ±0.69%    | fastest     |
| immutability-helper |     271 | ±0.33%    | 56% slower  |
+---------------------+---------+-----------+-------------+

Обратите внимание, что в Edge на той же машине Lodash работал хуже, чем два других метода. И в Firefox иногда cloneDeep является победителем, но в других случаях это Object.assign.

Вот ссылка на тест jsperf.com.

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

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

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

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

  • 0
    Object.assign не выполняет глубокое копирование. Скажем, если a = {c: {d: 1}} и b = Object.assign({}, a) , то вы выполняете bcd = 4 , тогда acd будет мутирован.
  • 0
    Вы правы, значение 1 самого внутреннего объекта ( acd ) будет видоизменено. Но если вы переназначите преемника первого уровня b , например, так: bc = {f: 1} , соответствующая часть a не будет мутирована (она останется {d: 1} ). Хороший улов в любом случае, я обновлю ответ прямо сейчас.
Показать ещё 2 комментария
26

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

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

   this.setState({items: items});
}
  • 2
    Нет, он Uncaught TypeError: Cannot read property 'items' of null .
  • 0
    Нет, не будет. Ваша ошибка, скорее всего, связана с тем, как вы делаете getInitialState .
Показать ещё 7 комментариев
20

Не мутируйте состояние на месте. Это может привести к неожиданным результатам. Я узнал мой урок! Всегда работайте с копией/клоном, 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

  • 7
    какие items в вашем примере? Вы this.state.items виду this.state.items или что-то еще?
12

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

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

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

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

const newItems = [...this.state.items];
newItems[item] = value;
this.setState({ items:newItems });
  • 0
    Это прямая мутация состояния и анти-паттерн React.
  • 8
    @TranslucentCloud - это, безусловно, не прямая мутация. исходный массив был клонирован, изменен, а затем состояние было установлено снова с использованием клонированного массива.
Показать ещё 3 комментария
6

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

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

Сначала вытащите весь объект 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
}
  • 0
    «Object.assign будет работать, если у вас есть относительно простая одноуровневая структура с глубоким состоянием, в которой самые внутренние члены содержат значения примитивного типа».
1

Без мутации:

// 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)
  • 2
    Я не могу видеть, где установлены newItems .
  • 0
    Разве карта плюс сравнение в массиве не ужасна для производительности?
0

Если вам нужно изменить только часть 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)
  • 2
    Это не работает, вы не можете вызвать карту после поиска
  • 0
    спасибо @paqash. Поменял его на фильтр.
Показать ещё 4 комментария
0

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

  • индекс компонента - index будет использоваться для создания/обновления в массиве.
  • set function - эта функция помещает данные в массив на основе индекса компонента.

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

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

import _ from 'lodash';

this.state.var_name  = _.assign(this.state.var_name, {
   obj_prop: 'changed_value',
});
  • 0
    Никогда не изменяйте состояние React напрямую.
0

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

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

  • 0
    Другое решение с использованием className jsfiddle.net/69z2wepo/6165
-2

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

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"/>
  • 0
    Никогда не изменяйте состояние React напрямую.

Ещё вопросы

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