如何在React中更新嵌套的状态属性

我试图通过使用这样的嵌套属性来组织我的状态:

this.state = {
someProperty: {
flag:true
}
}

但是像这样更新状态,

this.setState({ someProperty.flag: false });

是行不通的。怎样才能正确地做到这一点呢?

442178 次浏览

为了对嵌套对象进行setState,你可以遵循下面的方法,因为我认为setState不处理嵌套更新。

var someProperty = {...this.state.someProperty}
someProperty.flag = true;
this.setState({someProperty})

其思想是创建一个虚拟对象,对其执行操作,然后用更新的对象替换组件的状态

现在,展开操作符只创建对象的一层嵌套副本。如果你的状态是高度嵌套的,比如:

this.state = {
someProperty: {
someOtherProperty: {
anotherProperty: {
flag: true
}
..
}
...
}
...
}

你可以在每一层使用展开运算符setState

this.setState(prevState => ({
...prevState,
someProperty: {
...prevState.someProperty,
someOtherProperty: {
...prevState.someProperty.someOtherProperty,
anotherProperty: {
...prevState.someProperty.someOtherProperty.anotherProperty,
flag: false
}
}
}
}))

然而,随着状态嵌套越来越多,上面的语法变得越来越糟糕,因此我建议您使用immutability-helper包来更新状态。

关于如何使用immutability-helper更新状态,请参阅这个答案

有很多库可以提供帮助。例如,使用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},
});

把它写在一行里

this.setState({ someProperty: { ...this.state.someProperty, flag: false} });

如果你正在使用ES2015,你可以访问Object.assign。您可以使用它来更新嵌套对象。

this.setState({
someProperty: Object.assign({}, this.state.someProperty, {flag: false})
});

您将更新的属性与现有属性合并,并使用返回的对象更新状态。

编辑:将一个空对象作为目标添加到赋值函数中,以确保状态不会像carkod指出的那样直接发生突变。

这是第一个答案的变体,不需要任何额外的包、库或特殊函数。

state = {
someProperty: {
flag: 'string'
}
}


handleChange = (value) => {
const newState = {...this.state.someProperty, flag: value}
this.setState({ someProperty: newState })
}

为了设置特定嵌套字段的状态,您已经设置了整个对象。我通过创建一个变量newState,并使用ES2015 传播算子将当前状态的内容扩散到第一个中。然后,我用新值替换了this.state.flag的值(因为我设置了flag: value ,所以我将当前状态扩展到对象中,当前状态中的flag字段将被覆盖)。然后,我简单地将someProperty的状态设置为我的newState对象。

我用了这个方法。

如果你有一个这样的嵌套状态:

   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" />
const newState = Object.assign({}, this.state);
newState.property.nestedProperty = "new value";
this.setState(newState);

免责声明

React中的嵌套状态是错误的设计

# EYZ0阅读。

 

这个答案背后的原因是:

React的setState只是一个内置的方便,但你很快就会意识到 它有它的局限性。使用自定义属性和智能使用 forceUpdate给你更多。 如:< / p >
class MyClass extends React.Component {
myState = someObject
inputValue = 42
...

MobX,例如,完全抛弃状态并使用自定义可观察属性。
# EYZ0 < / p >

 


你痛苦的答案——参见这里的例子

还有另一种<强> < / >强短方法来更新任何嵌套的属性。

this.setState(state => {
state.nested.flag = false
state.another.deep.prop = true
return state
})

在一行上

 this.setState(state => (state.nested.flag = false, state))

注:这里是逗号操作符~MDN,看到它在行动here(沙盒).

它类似于(尽管这不会改变状态引用)

this.state.nested.flag = false
this.forceUpdate()

对于在这个上下文中forceUpdatesetState之间的微妙差异,请参阅链接示例沙盒

当然,这是在滥用一些核心原则,因为state应该是只读的,但由于您立即丢弃了旧的状态,并将其替换为新状态,因此这是完全可以的。

警告

即使组件包含状态更新和重新渲染正确(除了这个gotcha),道具将失败传播到子(见下面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.setStatethis.forceUpdate之后的情况。

改变沙盒状态的效果

使用嵌套的状态并直接改变状态是危险的,因为不同的对象可能(有意或无意)持有对状态的不同(旧的)引用,并且可能不一定知道何时更新(例如当使用PureComponent或如果shouldComponentUpdate被实现为返回false) 是为了显示旧的数据,如下例所示。

想象一下,一个应该呈现历史数据的时间轴,改变手上的数据将导致意想不到的行为,因为它也会改变之前的项目。

state-flow # EYZ0 < / p >

无论如何,在这里你可以看到Nested PureChildClass没有被重新渲染,因为道具未能传播。

我发现这对我很有用,在我的案例中有一个项目表单,例如,你有一个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>
)

让我知道!

还有两个选项没有提到:

  1. 如果您有深度嵌套的状态,请考虑是否可以重新构造子对象,使其位于根节点。这使得数据更容易更新。
  2. 有许多方便的库可用于处理不可变状态列在Redux文档中。我推荐Immer,因为它允许您以可变的方式编写代码,但在幕后处理必要的克隆。它还会冻结生成的对象,这样以后就不会意外地改变它。

为了让事情变得通用,我研究了@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);
}

注意:上述方法不适用于数组

有时候直接回答并不是最好的答案:)

短版:

这段代码

this.state = {
someProperty: {
flag: true
}
}

应该简化成这样吗

this.state = {
somePropertyFlag: true
}

长版:

目前# EYZ0。因为React不是面向嵌套状态的,这里提出的所有解决方案看起来都是hack。他们不使用框架,而是与之斗争。他们建议为了分组某些属性的可疑目的而编写不那么清晰的代码。所以它们作为挑战的答案是非常有趣的,但实际上毫无用处。

让我们想象一下下面的状态:

{
parent: {
child1: 'value 1',
child2: 'value 2',
...
child100: 'value 100'
}
}

如果只改变child1的值会发生什么?React不会重新渲染视图,因为它使用浅比较,它会发现parent属性没有改变。顺便说一句,直接改变状态对象通常被认为是一种糟糕的做法。

因此,您需要重新创建整个parent对象。但在这种情况下,我们会遇到另一个问题。React会认为所有的孩子都改变了他们的价值观,并重新渲染所有的孩子。当然,这对性能没有好处。

通过在shouldComponentUpdate()中编写一些复杂的逻辑仍然可以解决这个问题,但我更愿意在这里停下来,使用简短版本中的简单解决方案。

我们使用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;
}));
用immer你可以把你的状态作为一个“正常对象”来处理。

这样就足够了,

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)

创建一个状态的副本:

let someProperty = JSON.parse(JSON.stringify(this.state.someProperty))

对该对象进行更改:

someProperty.flag = "false"

现在更新状态

this.setState({someProperty})

我非常重视已经表达了关于创建组件状态的完整副本的问题。也就是说,我强烈建议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巧妙地使用代理对象来有效地复制任意深度状态树。Immer也比Immutability Helper等库更加类型安全,是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)}

我知道这是一个老问题,但仍然想分享我是如何做到这一点的。假设构造函数中的状态是这样的:

  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 = {
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')}
/>
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)
}

我用< em > < /减少em >搜索做嵌套更新:

例子:

状态中的嵌套变量:

state = {
coords: {
x: 0,
y: 0,
z: 0
}
}

< em >功能:< / em >

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

使用< em >: < / em >

<input
value={this.state.coords.x}
onChange={this.handleTextChange('coords.x')}
/>

虽然你问的是基于类的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 { useHookstate } from '@hookstate/core'


const state = useHookstate({
someProperty: {
flag: true,
otherNestedProp: 1
},
otherProp: 2
})

获取要渲染的字段:

state.someProperty.flag.get()
// or
state.get().someProperty.flag

设置嵌套字段:

state.someProperty.flag.set(false)

下面是Hookstate的例子,其中的状态被深深地/递归地嵌套在树形数据结构中。

这是我的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中设置初始值。状态

这里,初始值是在react状态下定义的

   state = {
data: {
fy: {
active: "N"
}
}
}

在formik initiValues属性中定义上述初始值

<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而不是# eyz1, formik setFieldValue函数来设置状态,或者使用react调试器工具来查看formik状态值中的变化。

试试下面的代码:

this.setState({ someProperty: {flag: false} });

这显然不是正确或最好的方法,但在我看来,这是更清晰的:

this.state.hugeNestedObject = hugeNestedObject;
this.state.anotherHugeNestedObject = anotherHugeNestedObject;


this.setState({})

然而,React本身应该迭代思想嵌套对象,并相应地更新状态和DOM。

你可以用对象扩展来做这个 代码:< / p >

 this.setState((state)=>({ someProperty:{...state.someProperty,flag:false}})

这将适用于更多嵌套的属性

用于多个输入控件和动态嵌套名称

<input type="text" name="title" placeholder="add title" onChange={this.handleInputChange} />
<input type="checkbox" name="chkusein" onChange={this.handleInputChange} />
<textarea name="body" id="" cols="30" rows="10" placeholder="add blog content" onChange={this.handleInputChange}></textarea>

代码非常易读

处理程序

handleInputChange = (event) => {
const target = event.target;
const value = target.type === 'checkbox' ? target.checked : target.value;
const name = target.name;
const newState = { ...this.state.someProperty, [name]: value }
this.setState({ someProperty: newState })
}

还有另一个选项,如果对象列表中有多个项目,这是有效的:使用this.state. obj将对象复制到一个变量(比如temp),使用filter()方法遍历对象并获取您想要更改为一个对象的特定元素(命名为updateObj),并将剩余的对象列表添加到另一个对象(命名为restObj)。现在编辑你想要更新的对象的内容,创建一个新项目(比如newItem)。然后调用this.setUpdate()并使用展开操作符将新的对象列表传递给父对象。

this.state = {someProperty: { flag:true, }}




var temp=[...this.state.someProperty]
var restObj = temp.filter((item) => item.flag !== true);
var updateObj = temp.filter((item) => item.flag === true);


var newItem = {
flag: false
};
this.setState({ someProperty: [...restObj, newItem] });

根据框架的标准,不确定这在技术上是否正确,但有时您只是需要更新嵌套对象。这是我使用钩子的解决方案。

setInputState({
...inputState,
[parentKey]: { ...inputState[parentKey], [childKey]: value },
});

我看到每个人都给出了基于类的组件状态更新解决方案,这是预期的,因为他要求,但我试图给钩子相同的解决方案。

const [state, setState] = useState({
state1: false,
state2: 'lorem ipsum'
})

现在如果你想改变嵌套对象键< >强state1 < / >强,那么你可以做以下任何一项:

< >强过程1 < / >强

let oldState = state;
oldState.state1 = true
setState({...oldState);

< >强过程2 < / >强

setState(prevState => ({
...prevState,
state1: true
}))

我最喜欢过程。

如果你想动态设置状态


下面的示例动态设置表单的状态,其中状态中的每个键都是object

 onChange(e:React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) {
this.setState({ [e.target.name]: { ...this.state[e.target.name], value: e.target.value } });
}
你应该将new state传递给setState。

. new state的引用必须与old state的引用不同

所以试试这个:

this.setState({
...this.state,
someProperty: {...this.state.someProperty, flag: true},
})

下面是一个使用嵌套状态(一层)的完整示例,使用这个答案中的解来实现作为类的组件:

class CaveEditModal extends React.Component {


// ...


constructor(props, context) {
super(props);
this.state = {


tabValue: '1',
isModalOpen: this.props.isModalOpen,


// ...
caveData: {
latitude: 1,
longitude: 2
}
};


// ...


const updateNestedFieldEvent = fullKey => ev => {
      

var [parentProperty, _key] = fullKey.split(".", 2);


this.setState({[parentProperty]: { ...this.state[parentProperty], [_key]: ev.target.value} });
};
// ...


this.handleLatitudeChange = updateNestedFieldEvent('caveData.latitude');
this.handleLongitudeChange = updateNestedFieldEvent('caveData.longitude');
}


render () {
return (
<div>
<TextField id="latitude" label="Latitude" type="number" value={this.state.caveData.latitude} onChange={this.handleLatitudeChange} />
<TextField id="longitude" label="Longitude" type="number" value={this.state.caveData.longitude} onChange={this.handleLongitudeChange} />
<span>lat={this.state.caveData.latitude} long={this.state.caveData.longitude}</span>
</div>
);
};


}

注意,状态更新器函数updateNestedFieldEvent只适用于a.b这样的一级嵌套对象,而不是a.b.c

setInputState((pre)=> ({...pre,[parentKey]: {...pre[parentKey], [childKey]: value}}));

我喜欢这个

对于在2022年读书的人来说:

    constructor(props) {
super(props);
this.state = {
someProperty: {
flag: true
}
otherValues: '',
errors: {}
};


this.handleInputChange = this.handleInputChange.bind(this);
}


handleInputChange(event) {
const target = event.target;
const value = target.type === 'checkbox' ? target.checked : target.value;
const name = target.name;
const someProperty = { ...this.state.someProperty };
someProperty[name] = value;


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