Я создаю приложение, в котором пользователь может создать свою собственную форму. Например, укажите название поля и детали, какие другие столбцы должны быть включены.
Компонент доступен как 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
?
Для этого можно использовать 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 от родителя, содержащего состояние, к дочернему компоненту, который должен вызвать изменение состояния.
update
возникла эта ошибка: Uncaught ReferenceError: update is not defined
React.addons.update
Неправильный способ!
С 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
},
}));
};
Поскольку в этой теме много дезинформации, вот как вы можете сделать это без вспомогательных библиотек:
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
Для модификации глубоко вложенных объектов/переменных в состоянии React обычно используются три метода: vanilla JS Object.assign
, immutability-helper и cloneDeep
из Lodash. Для этого есть множество других менее популярных сторонних библиотек, но в этом ответе я расскажу только об этих трех вариантах. Кроме того, есть некоторые методы, которые используют не что иное, как ванильный JavaScript, например распространение массива (см., Например, ответ @mpen), но они не очень интуитивны, просты в использовании и способны обрабатывать все ситуации с манипуляциями с состоянием.
Как отмечалось, бесчисленное количество раз в самых популярных комментариях к ответам, авторы которых предлагают прямую мутацию состояния, просто не делайте этого. Это вездесущий анти-паттерн React, который неизбежно приведет вас к нежелательным последствиям. Учитесь правильно.
Давайте сравним три широко используемых метода.
Учитывая это государственное устройство:
state = {
foo: {
bar: 'initial value'
}
};
(...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
. Вы рискуете мутировать государство напрямую.
(...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
выполняет глубокое клонирование, поэтому это надежный вариант, если у вас достаточно сложное состояние с многоуровневыми объектами или массивами.
(...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
, это очень продвинутый инструмент, который можно использовать для манипулирования состоянием.
Object.assign
не выполняет глубокое копирование. Скажем, если a = {c: {d: 1}}
и b = Object.assign({}, a)
, то вы выполняете bcd = 4
, тогда acd
будет мутирован.
1
самого внутреннего объекта ( acd
) будет видоизменено. Но если вы переназначите преемника первого уровня b
, например, так: bc = {f: 1}
, соответствующая часть a
не будет мутирована (она останется {d: 1}
). Хороший улов в любом случае, я обновлю ответ прямо сейчас.
Сначала получите нужный элемент, измените то, что вы хотите на этом объекте, и верните его в состояние.
То, как вы используете состояние, только передавая объект в getInitialState
, было бы проще, если бы вы использовали ключевой объект.
handleChange: function (e) {
item = this.state.items[1];
item.name = 'newName';
items[1] = item;
this.setState({items: items});
}
Uncaught TypeError: Cannot read property 'items' of null
.
getInitialState
.
Не мутируйте состояние на месте. Это может привести к неожиданным результатам. Я узнал мой урок! Всегда работайте с копией/клоном, 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
items
в вашем примере? Вы this.state.items
виду this.state.items
или что-то еще?
Согласно документации React для setState, использование Object.assign
как предлагается другими ответами, здесь не идеально. Из-за характера асинхронного поведения setState
последующие вызовы, использующие эту технику, могут переопределять предыдущие вызовы, вызывая нежелательные результаты.
Вместо этого в документах React рекомендуется использовать форму обновления setState
которая работает в предыдущем состоянии. Имейте в виду, что при обновлении массива или объекта вы должны возвращать новый массив или объект, поскольку React требует от нас сохранения неизменности состояния. Использование оператора расширения синтаксиса ES6 для поверхностного копирования массива, создание или обновление свойства объекта по заданному индексу массива будет выглядеть так:
this.setState(prevState => {
const newItems = [...prevState.items];
newItems[index].name = newName;
return {items: newItems};
})
У меня такая же проблема. Вот простое решение, которое работает!
const newItems = [...this.state.items];
newItems[item] = value;
this.setState({ items:newItems });
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 });
}
Это действительно просто.
Сначала вытащите весь объект 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
}
Без мутации:
// 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)
newItems
.
Если вам нужно изменить только часть 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)
Как создать другой компонент (для объекта, который должен войти в массив) и передать в качестве реквизита следующее?
<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})}/>
Попробуйте это, это определенно сработает, в другом случае я попытался, но не работал.
import _ from 'lodash';
this.state.var_name = _.assign(this.state.var_name, {
obj_prop: 'changed_value',
});
Используйте событие на handleChange
, чтобы выяснить элемент, который был изменен, а затем обновить его. Для этого вам может потребоваться изменить какое-либо свойство, чтобы идентифицировать его и обновить.
Смотрите скрипку https://jsfiddle.net/69z2wepo/6164/
className
jsfiddle.net/69z2wepo/6165
Я бы переместил изменение дескриптора функции и добавил индексный параметр
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"/>
PopulateAtCheckboxes
компонент в отредактированном вопрос связан сDynamicForm
компонентом , который держит состояние вы пытаетесь изменить.