我正在创建一个应用程序,用户可以在其中设计自己的表单。例如,指定字段的名称和应包含的其他列的详细信息。
该组件可作为 JSFiddle here 使用。
我的初始状态如下所示:
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
?
由于该线程中有很多错误信息,因此您可以在没有帮助库的情况下执行以下操作:
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's 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
一个数组。 OP使用了一个对象。但是,概念是相同的。
您可以在终端/控制台中看到发生了什么:
❯ 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
您可以使用 update
immutability helper for this:
this.setState({
items: update(this.state.items, {1: {name: {$set: 'updated field name'}}})
})
或者,如果您不关心能否使用 ===
在 shouldComponentUpdate()
生命周期方法中检测到此项目的更改,您可以直接编辑状态并强制组件重新渲染 - 这实际上与 @ 相同聚光灯的答案,因为它正在将一个对象拉出状态并对其进行编辑。
this.state.items[1].name = 'updated field name'
this.forceUpdate()
后期编辑补充:
查看 react-training 中的 Simple Component Communication 课程,了解如何将回调函数从持有状态的父组件传递到需要触发状态更改的子组件。
错误的方法!
handleChange = (e) => {
const { items } = this.state;
items[1].name = e.target.value;
// update state
this.setState({
items,
});
};
正如许多更好的开发人员在评论中指出的那样:改变状态是错误的!
我花了一段时间才弄清楚这一点。以上工作,但它带走了 React 的力量。例如,componentDidUpdate
不会将此视为更新,因为它是直接修改的。
所以正确的方法是:
handleChange = (e) => {
this.setState(prevState => ({
items: {
...prevState.items,
[prevState.items[1].name]: e.target.value,
},
}));
};
items[1].role = e.target.value
变异状态吗?
要修改 React 状态中深度嵌套的对象/变量,通常使用三种方法:vanilla JavaScript 的 Object.assign
、immutability-helper 和 Lodash 中的 cloneDeep
。
还有很多其他不太受欢迎的第三方库可以实现这一点,但在这个答案中,我将只介绍这三个选项。此外,还存在一些额外的 vanilla JavaScript 方法,例如数组传播(例如,参见 @mpen 的答案),但它们不是很直观、易于使用并且能够处理所有状态操作情况。
正如在对答案的最高投票评论中无数次指出的那样,其作者提出了状态的直接突变:不要那样做。这是一个无处不在的 React 反模式,它不可避免地会导致不想要的后果。学习正确的方法。
让我们比较三种广泛使用的方法。
给定这个状态对象结构:
state = {
outer: {
inner: 'initial value'
}
}
您可以使用以下方法更新最内层 inner
字段的值,而不会影响状态的其余部分。
1. Vanilla JavaScript 的 Object.assign
const App = () => { const [outer, setOuter] = React.useState({ inner: '初始值' }) React.useEffect(() => { console.log('浅拷贝前:', outer .inner) // 初始值 const newOuter = Object.assign({}, outer, { inner: 'updated value' }) console.log('浅拷贝取完后,状态下的值还是:', outer.inner) // 初始值 setOuter(newOuter) }, []) console.log('In render:', outer.inner) return (
请记住,自 it only copies property values 以来,Object.assign不会执行深度克隆,这就是为什么它所做的操作称为浅复制(请参阅评论)。
为此,我们应该只操作 primitive 类型 (outer.inner
) 的属性,即字符串、数字、布尔值。
在此示例中,我们使用 Object.assign
创建一个新常量 (const newOuter...
),它创建一个空对象 ({}
),将 outer
对象 ({ inner: 'initial value' }
) 复制到其中,然后复制一个不同的对象{ inner: 'updated value' }
超过它。
这样,最终新创建的 newOuter
常量将保持 { inner: 'updated value' }
的值,因为 inner
属性已被覆盖。这个 newOuter
是一个全新的对象,它没有链接到状态中的对象,因此可以根据需要对其进行变异,并且状态将保持不变并且在运行更新它的命令之前不会更改。
最后一部分是使用 setOuter()
setter 将状态中的原始 outer
替换为新创建的 newOuter
对象(只有值会改变,属性名称 outer
不会)。
现在想象我们有一个更深的状态,例如 state = { outer: { inner: { innerMost: 'initial value' } } }
。我们可以尝试创建 newOuter
对象并使用状态中的 outer
内容填充它,但是 Object.assign
将无法将 innerMost
的值复制到这个新创建的 newOuter
对象,因为 {5 } 嵌套太深。
您仍然可以像上面的示例一样复制 inner
,但由于它现在是一个对象并且不是原语,来自 newOuter.inner
的 reference 将被复制到outer.inner
代替,这意味着我们最终将本地 newOuter
对象直接绑定到状态中的对象。
这意味着在这种情况下,本地创建的 newOuter.inner
的突变将直接影响 outer.inner
对象(处于状态),因为它们实际上变成了相同的东西(在计算机的内存中)。
因此,Object.assign
仅在您具有相对简单的一级深层状态结构且最里面的成员持有原始类型的值时才有效。
如果您有更深层次的对象(第 2 级或更高级别)需要更新,请不要使用 Object.assign
。你冒着直接改变状态的风险。
2. Lodash 的 cloneDeep
const App = () => { const [outer, setOuter] = React.useState({ inner: '初始值' }) React.useEffect(() => { console.log('深度克隆前:', external .inner) // 初始值 const newOuter = _.cloneDeep(outer) // cloneDeep() 来自 Lodash lib newOuter.inner = 'updated value' console.log('深度克隆对象修改后,值in the state is still:', outer.inner) // 初始值 setOuter(newOuter) }, []) console.log('In render:', outer.inner) return (
Lodash 的 cloneDeep 使用起来更加简单。它执行深度克隆,因此如果您有一个相当复杂的状态,其中包含多级对象或数组,那么它是一个强大的选项。只需 cloneDeep()
顶级状态属性,以您喜欢的任何方式改变克隆部分,然后 setOuter()
将其返回到状态。
3. 不变性助手
const App = () => { const [outer, setOuter] = React.useState({ inner: '初始值' }) React.useEffect(() => { const update = immutabilityHelper console.log('深度克隆之前and updates:', outer.inner) // 初始值 const newOuter = update(outer, { inner: { $set: 'updated value' } }) console.log('克隆更新后状态下的值is still:', outer.inner) // 初始值 setOuter(newOuter) }, []) console.log('In render:', outer.inner) return (
immutability-helper
将它提升到一个全新的水平,它的酷炫之处在于它不仅可以用 $set
值来声明项目,还可以用 $push
、$splice
、$merge
(等)它们。这里有一个可用的 list of commands。
旁注
同样,请记住,setOuter
仅修改状态对象(在这些示例中为 outer
)的一级属性,而不是深度嵌套的(outer.inner
)。如果它以不同的方式表现,这个问题就不会存在。
哪一个适合您的项目?
如果您不想或不能使用外部依赖项,并且拥有简单的状态结构,请坚持使用Object.assign
。
如果您操纵一个巨大和/或复杂的状态,Lodash 的 cloneDeep
是一个明智的选择。
如果您需要高级功能,即如果您的状态结构很复杂,并且需要对其执行各种操作,请尝试immutability-helper
,它是一个非常先进的工具,可用于状态操作。
...或者,您真的需要这样做吗?
如果你在 React 的状态下持有一个复杂的数据,也许现在是考虑其他处理它的方法的好时机。在 React 组件中设置复杂的状态对象并不是一个简单的操作,我强烈建议考虑不同的方法。
很可能你最好不要将复杂数据保存在 Redux 存储中,使用 reducer 和/或 sagas 将其设置在那里,并使用选择器访问它。
Object.assign
不执行深层复制。比如说,如果 a = {c: {d: 1}}
和 b = Object.assign({}, a)
,那么你执行 b.c.d = 4
,那么 a.c.d
就会发生变异。
a.c.d
) 的值 1
会发生变异。但是,如果您将重新分配 b
的第一级后继,例如:b.c = {f: 1}
,则 a
的相应部分将不会发生突变(它将保持 {d: 1}
)。无论如何都很好,我会立即更新答案。
shallow copy
而不是 deep copy
。 shallow copy
的含义很容易混淆。在 shallow copy
中,a !== b
,但对于来自源对象 a
的每个键,a[key] === b[key]
Object.assign
的浅薄性。
JSON.parse(JSON.stringify(object))
也是深度克隆的变体。虽然性能比 lodash cloneDeep
差。 measurethat.net/Benchmarks/Show/2751/0/…
我有同样的问题。这是一个有效的简单解决方案!
const newItems = [...this.state.items];
newItems[item] = value;
this.setState({ items:newItems });
{
花括号而不是我已经补救的括号
根据 setState 上的 React 文档,此处其他答案所建议的使用 Object.assign
并不理想。由于 setState
的异步行为的性质,使用此技术的后续调用可能会覆盖先前的调用,从而导致不良结果。
相反,React 文档建议使用更新程序形式的 setState
,它在先前的状态上运行。请记住,在更新数组或对象时您必须返回一个新的数组或对象,因为 React 要求我们保持状态不变性。使用 ES6 语法的扩展运算符对数组进行浅拷贝,在数组的给定索引处创建或更新对象的属性如下所示:
this.setState(prevState => {
const newItems = [...prevState.items];
newItems[index].name = newName;
return {items: newItems};
})
首先获取您想要的项目,更改您想要的对象并将其设置回状态。如果您使用键控对象,那么您通过仅在 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
的方式。
items
未在任何地方定义。
item
分配了对状态自己的 this.state.items[1]
变量的引用。然后修改 item
(item.name = 'newName'
),从而直接改变状态,这是非常不鼓励的。在您的示例中,甚至不需要调用 this.setState({items: items})
,因为 state 已经直接改变了。
不要改变现有的状态。它可能会导致意想不到的结果。我已经吸取了教训!始终使用副本/克隆,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
还是别的什么?
items[1] = item;
上方应该有一行显示 items = this.state.items;
。当心我的javascript生锈了,我正在为我的家庭项目学习反应,所以我不知道这是好是坏:-)
在一行中使用带有箭头功能的数组映射
this.setState({
items: this.state.items.map((item, index) =>
index === 1 ? { ...item, name: 'newName' } : item,
)
})
有时在 React 中,对克隆的数组进行变异会影响原始数组,这种方法永远不会导致变异:
const myNewArray = Object.assign([...myArray], {
[index]: myNewItem
});
setState({ myArray: myNewArray });
或者,如果您只想更新项目的属性:
const myNewArray = Object.assign([...myArray], {
[index]: {
...myArray[index],
prop: myNewValue
}
});
setState({ myArray: myNewArray });
由于上述选项对我来说都不理想,我最终使用了 map:
this.setState({items: this.state.items.map((item,idx)=> idx!==1 ?item :{...item,name:'new_name'}) })
无突变:
// 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
的设置位置。
fo of
或 forEach
map 是最快的感到困惑。
这真的很简单。
首先从 state 中拉出整个 items 对象,根据需要更新 items 对象的一部分,然后通过 setState 将整个 items 对象恢复到 state。
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
}
发现这出人意料地困难,并且 ES6 传播魔法似乎都没有按预期工作。正在使用这样的结构来获取渲染的元素属性以用于布局目的。
发现使用 immutability-helper
中的 update
方法是这个简化示例中最直接的方法:
constructor(props) {
super(props)
this.state = { values: [] }
this.updateContainerState = this.updateContainerState.bind(this)
}
updateContainerState(index, value) {
this.setState((state) => update(state, { values: { [index]: { $set: value } } }))
}
改编自https://github.com/kolodny/immutability-helper#computed-property-names
要更新的数组成员是一个更嵌套的复杂对象,使用基于复杂性的appropriate deep copy method。
肯定有更好的方法来处理布局参数,但这是关于如何处理数组的。每个子元素的相关值也可以在它们之外计算,但我发现将 containerState 向下传递更方便,因此它们的子元素可以随意获取属性并在给定索引处更新父状态数组。
import React from 'react'
import update from 'immutability-helper'
import { ContainerElement } from './container.component.style.js'
import ChildComponent from './child-component'
export default class ContainerComponent extends React.Component {
constructor(props) {
super(props)
this.state = { values: [] }
this.updateContainerState = this.updateContainerState.bind(this)
}
updateContainerState(index, value) {
this.setState((state) => update(state, { values: { [index]: { $set: value } } }))
}
// ...
render() {
let index = 0
return (
<ContainerElement>
<ChildComponent
index={index++}
containerState={this.state}
updateContainerState={this.updateContainerState}
/>
<ChildComponent
index={index++}
containerState={this.state}
updateContainerState={this.updateContainerState}
/>
</ContainerElement>
)
}
}
使用 handleChange
上的事件找出已更改的元素,然后对其进行更新。为此,您可能需要更改某些属性以识别并更新它。
见小提琴https://jsfiddle.net/69z2wepo/6164/
我会移动函数句柄更改并添加索引参数
handleChange: function (index) {
var items = this.state.items;
items[index].name = 'newName';
this.setState({items: items});
},
到动态表单组件并将其作为道具传递给 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"/>
如果您只需要更改 Array
的一部分,则您有一个状态设置为的反应组件。
state = {items: [{name: 'red-one', value: 100}, {name: 'green-one', value: 999}]}
最好按如下方式更新 Array
中的 red-one
:
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)
newArray
是什么?你是说newItems
吗?如果你这样做了,那之后会不会只留下一个项目?
state
对象引入一个新属性 newItems
,并且不会更新现有的 items
属性。
或者如果您有一个动态生成的列表并且您不知道索引但只有键或 ID:
let ItemsCopy = []
let x = this.state.Items.map((entry) =>{
if(entry.id == 'theIDYoureLookingFor')
{
entry.PropertyToChange = 'NewProperty'
}
ItemsCopy.push(entry)
})
this.setState({Items:ItemsCopy});
尝试使用代码:
this.state.items[1] = 'new value';
var cloneObj = Object.assign({}, this.state.items);
this.setState({items: cloneObj });
在我沉闷的大脑中,下面的一段代码很容易。删除对象并替换为更新的对象
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)
}));
如何创建另一个组件(对于需要进入数组的对象)并将以下内容作为道具传递?
组件索引 - 索引将用于在数组中创建/更新。 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})}/>
this.setState({
items: this.state.items.map((item,index) => {
if (index === 1) {
item.name = 'newName';
}
return item;
})
});
item = Object.assign({}, item, {name: 'newName'});
@JonnyBuchanan 的答案完美无缺,但仅适用于数组状态变量。如果状态变量只是一个字典,请遵循以下命令:
inputChange = input => e => {
this.setState({
item: update(this.state.item, {[input]: {$set: e.target.value}})
})
}
您可以将 [input]
替换为字典的字段名称,并将 e.target.value
替换为其值。此代码对我的表单的输入更改事件执行更新作业。
handleChanges = (value, key) => {
// clone the current State object
let cloneObject = _.extend({}, this.state.currentAttribute);
// key as user.name and value= "ABC" then current attributes have current properties as we changes
currentAttribute[key] = value;
// then set the state "currentAttribute" is key and "cloneObject" is changed object.
this.setState({currentAttribute: cloneObject});
和从文本框中更改添加 onChange 事件
onChange = {
(event) => {
this.handleChanges(event.target.value, "title");
}
}
试试这个它肯定会工作,其他情况我试过但没用
import _ from 'lodash';
this.state.var_name = _.assign(this.state.var_name, {
obj_prop: 'changed_value',
});
不定期副业成功案例分享
items[0] === clone[0]
位。三重=
检查对象是否引用同一事物。items.findIndex()
应该做的很短。