我试图通过使用这样的嵌套属性来组织我的状态:
this.state = {
someProperty: {
flag:true
}
}
但是像这样更新状态
this.setState({ someProperty.flag: false });
不起作用。如何正确完成?
我试图通过使用这样的嵌套属性来组织我的状态:
this.state = {
someProperty: {
flag:true
}
}
但是像这样更新状态
this.setState({ someProperty.flag: false });
不起作用。如何正确完成?
Answers:
为了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
package更新状态。
有关如何使用更新状态的信息,请参见此答案immutability helper
。
someProperty
,所以我们不能直接访问该州,我们正在对此进行修改
this.setState((prevState) => ({nested: {...prevState.nested, propertyToSet: newValue}})
虽然有点乏味……
...prevState,
在最后一个示例中不需要,仅您someProperty
和它的孩子
一行编写
this.setState({ someProperty: { ...this.state.someProperty, flag: false} });
this.state
在此之后立即阅读setState
并期望它具有新的价值。对战通话this.state
中setState
。还是后者还有一个我不清楚的问题?
this.state
after之后的访问问题setState
。此外,我们没有传递this.state
给setState
。该…
运营商扩展性。它与Object.assign()相同。github.com/tc39/proposal-object-rest-spread
this.setState(currentState => { someProperty: { ...currentState.someProperty, flag: false} });
可以避免这个问题?
有时直接答案不是最佳答案:)
简洁版本:
此代码
this.state = {
someProperty: {
flag: true
}
}
应该简化为类似
this.state = {
somePropertyFlag: true
}
长版:
当前,您不应该在React中使用嵌套状态。因为React不适合与嵌套状态一起使用,并且这里提出的所有解决方案看起来都是骇客。他们不使用框架而是与之抗争。他们建议编写不太清晰的代码,以便对某些属性进行分组。因此,它们作为应对挑战的方式非常有趣,但实际上没有用。
让我们想象一下以下状态:
{
parent: {
child1: 'value 1',
child2: 'value 2',
...
child100: 'value 100'
}
}
如果仅更改一个值,将会发生什么child1
?React不会重新渲染视图,因为它使用浅表比较,并且会发现该parent
属性没有改变。通常,将BTW直接更改为状态对象是一种不良做法。
所以你需要重新创建整体 parent
对象。但是在这种情况下,我们将遇到另一个问题。React会认为所有孩子都改变了他们的价值观,并将重新渲染他们的所有价值观。当然,这对性能不利。
仍然可以通过编写一些复杂的逻辑来解决该问题,shouldComponentUpdate()
但我宁愿在此停止并使用简短版本中的简单解决方案。
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))
注意:这是逗号运算符〜MDN,在这里可以看到它的作用(沙盒)。
它类似于(尽管这不会更改状态引用)
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
)或有旨在显示旧数据,如以下示例所示。
想象一下应该渲染历史数据的时间轴,对手底数据进行突变会导致意外行为,因为它还会更改以前的项目。
无论如何,您可以在这里看到Nested PureChildClass
由于道具传播失败而未重新渲染。
state
完全沟渠并使用自定义的可观察属性。React setState
只是内置的便利,但是您很快就会意识到它有其局限性。使用自定义属性和智能使用可以forceUpdate
给您带来更多的收益。使用Observables代替React组件中的状态。
this.forceUpdate()
或实现specific shouldComponentUpdate
。
如果您使用的是ES2015,则可以访问Object.assign。您可以如下使用它来更新嵌套对象。
this.setState({
someProperty: Object.assign({}, this.state.someProperty, {flag: false})
});
您将更新后的属性与现有属性合并,并使用返回的对象来更新状态。
编辑:将一个空对象作为目标分配给assign函数,以确保状态不会像carkod所指出的那样直接突变。
const newState = Object.assign({}, this.state);
newState.property.nestedProperty = "new value";
this.setState(newState);
property
包含多个嵌套属性,则第一个将删除它们,第二个则不会。codeandbox.io/s/nameless-pine-42thu
有很多库可以帮助您解决此问题。例如,使用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},
});
lodash
,则可能要尝试_.cloneDeep
将state设置为克隆副本。
lodash/fp
,所以我们不必担心突变。
merge
或set
运行还有什么好处?
this.setState(newState)
lodash / fp方法。另一个陷阱是,lodash .set
(非fp)具有不同顺序的参数,这使我绊了一会儿。
我们使用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;
}));
通过沉浸,您可以将状态视为“正常对象”。魔术发生在具有代理对象的幕后。
这是此线程中给出的第一个答案的变体,不需要任何额外的程序包,库或特殊功能。
state = {
someProperty: {
flag: 'string'
}
}
handleChange = (value) => {
const newState = {...this.state.someProperty, flag: value}
this.setState({ someProperty: newState })
}
为了设置特定嵌套字段的状态,您已经设置了整个对象。为此,我创建了一个变量,newState
并首先使用ES2015 Spread 运算符将当前状态的内容扩展到其中。然后,我用this.state.flag
新值替换了值(因为flag: value
在将当前状态散布到对象中后进行设置,所以当前状态中的flag
字段将被覆盖)。然后,我简单地设置的状态someProperty
到我的newState
对象。
尽管嵌套并不是您应该如何对待组件状态,但有时对于单层嵌套来说还是很容易的。
对于这样的状态
state = {
contact: {
phone: '888-888-8888',
email: 'test@test.com'
}
address: {
street:''
},
occupation: {
}
}
我所使用的可重用方法如下所示。
handleChange = (obj) => e => {
let x = this.state[obj];
x[e.target.name] = e.target.value;
this.setState({ [obj]: x });
};
然后只需为您要寻址的每个嵌套传递obj名称...
<TextField
name="street"
onChange={handleChange('address')}
/>
我用了这个解决方案。
如果您有这样的嵌套状态:
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" />
尽管您询问了基于类的React组件的状态,但useState钩子也存在相同的问题。更糟糕的是:useState钩子不接受部分更新。因此,当引入useState挂钩时,此问题变得非常相关。
我决定发布以下答案,以确保问题涵盖使用useState挂钩的更现代的方案:
如果您有:
const [state, setState] = useState({ someProperty: { flag: true, otherNestedProp: 1 }, otherProp: 2 })
您可以通过克隆当前数据并修补所需的数据段来设置嵌套属性,例如:
setState(current => { ...current, someProperty: { ...current.someProperty, flag: false } });
或者,您可以使用Immer库来简化对象的克隆和修补。
或者,您可以使用Hookstate库(免责声明:我是作者)来简单地完全管理复杂(本地和全局)状态数据并提高性能(阅读:不必担心渲染优化):
import { useStateLink } from '@hookstate/core'
const state = useStateLink({ someProperty: { flag: true, otherNestedProp: 1 }, otherProp: 2 })
获取要渲染的字段:
state.nested.someProperty.nested.flag.get()
// or
state.get().someProperty.flag
设置嵌套字段:
state.nested.someProperty.nested.flag.set(false)
这是Hookstate的示例,其中状态被深度/递归嵌套在树状数据结构中。
尚未提及的其他两个选项:
为了使事情变得通用,我研究了@ShubhamKhatri和@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);
}
注意:以上方法不适用于数组
我非常重视围绕创建组件状态的完整副本已经表达的担忧。话虽如此,我强烈建议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
巧妙地使用了代理对象来有效地复制任意深层状态树。与Immutability Helper之类的库相比,Immer还具有更好的类型安全性,是Javascript和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)}
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)
}
obj
这只是对国家的引用,因此您正在通过它来改变实际的this.state
对象
我发现这对我有用,在我的情况下有一个项目表单,例如您有一个ID和一个名称,而我宁愿维护嵌套项目的状态。
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>
)
让我知道!
这样的事情可能就足够了,
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)
我知道这是一个古老的问题,但仍然想分享我是如何实现的。假设构造函数中的状态如下所示:
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"
/>
我使用减少搜索来嵌套更新:
例:
嵌套变量的状态:
state = {
coords: {
x: 0,
y: 0,
z: 0
}
}
功能:
handleChange = nestedAttr => event => {
const { target: { value } } = event;
const attrs = nestedAttr.split('.');
let stateVar = this.state[attrs[0]];
if(attrs.length>1)
attrs.reduce((a,b,index,arr)=>{
if(index==arr.length-1)
a[b] = value;
else if(a[b]!=null)
return a[b]
else
return a;
},stateVar);
else
stateVar = value;
this.setState({[attrs[0]]: stateVar})
}
采用:
<input
value={this.state.coords.x}
onChange={this.handleTextChange('coords.x')}
/>
这是我的initialState
const initialStateInput = {
cabeceraFamilia: {
familia: '',
direccion: '',
telefonos: '',
email: ''
},
motivoConsulta: '',
fechaHora: '',
corresponsables: [],
}
挂钩或您可以将其替换为状态(类组件)
const [infoAgendamiento, setInfoAgendamiento] = useState(initialStateInput);
handleChange的方法
const actualizarState = e => {
const nameObjects = e.target.name.split('.');
const newState = setStateNested(infoAgendamiento, nameObjects, e.target.value);
setInfoAgendamiento({...newState});
};
嵌套状态设置状态的方法
const setStateNested = (state, nameObjects, value) => {
let i = 0;
let operativeState = state;
if(nameObjects.length > 1){
for (i = 0; i < nameObjects.length - 1; i++) {
operativeState = operativeState[nameObjects[i]];
}
}
operativeState[nameObjects[i]] = value;
return state;
}
最后,这是我使用的输入
<input type="text" className="form-control" name="cabeceraFamilia.direccion" placeholder="Dirección" defaultValue={infoAgendamiento.cabeceraFamilia.direccion} onChange={actualizarState} />
如果您在项目中使用formik,则有一些简单的方法可以处理这些问题。这是使用Formik最简单的方法。
首先在formik initivalues属性或react中设置您的初始值。州
在此,初始值在反应状态下定义
state = {
data: {
fy: {
active: "N"
}
}
}
在formik initiValues
属性内为formik字段定义上述initialValues
<Formik
initialValues={this.state.data}
onSubmit={(values, actions)=> {...your actions goes here}}
>
{({ isSubmitting }) => (
<Form>
<Field type="checkbox" name="fy.active" onChange={(e) => {
const value = e.target.checked;
if(value) setFieldValue('fy.active', 'Y')
else setFieldValue('fy.active', 'N')
}}/>
</Form>
)}
</Formik>
使控制台检查更新到的状态,string
而不是boolean
使用formik setFieldValue
函数来设置状态,或者使用react调试器工具查看formik状态值中的更改。