我一直在考虑使用React setState()方法更新这些嵌套属性的最佳选择。我也愿意考虑更高效的方法来考虑性能,并避免与其他可能的并发状态更改发生冲突。
注意:我正在使用扩展React.Component
的类组件。如果您使用的是React.PureComponent
,则在更新嵌套属性时必须格外小心,因为如果不更改state
的任何顶级属性,这可能不会触发重新渲染。这是说明此问题的沙箱:
CodeSandbox - Component vs PureComponent and nested state changes
回到这个问题-我的关注点是在状态上更新嵌套属性时,性能以及其他并发setState()
调用之间可能存在的冲突
示例:
假设我正在构建一个表单组件,并使用以下对象初始化表单状态:
this.state = {
isSubmitting: false,
inputs: {
username: {
touched: false,
dirty: false,
valid: false,
invalid: false,
value: 'some_initial_value'
},
email: {
touched: false,
dirty: false,
valid: false,
invalid: false,
value: 'some_initial_value'
}
}
}
根据我的研究,通过使用setState()
,React将浅合并我们传递给它的对象,这意味着它仅用于检查顶级属性,例如isSubmitting
和inputs
。
因此,我们可以将包含这两个顶级属性(isSubmitting
和inputs
)的完整newState对象传递给它,也可以传递这些属性之一并将其浅层合并到以前的状态。
问题1
您是否同意最佳做法是仅传递我们正在更新的state
顶级属性?例如,如果我们不更新isSubmitting
属性,则应避免将其传递给setState()
,以避免与可能已排队的其他对setState()
的并发调用发生冲突/覆盖和这个一起吗?它是否正确?
在此示例中,我们将传递仅具有inputs
属性的对象。这样可以避免与可能试图更新setState()
属性的另一个isSubmitting
发生冲突/覆盖。
问题2
从性能角度来看,复制当前状态以更改其嵌套属性的最佳方法是什么?
在这种情况下,假设我要设置state.inputs.username.touched = true
。
即使您可以这样做:
this.setState( (state) => {
state.inputs.username.touched = true;
return state;
});
不应该。因为从React Docs,我们有:
状态 是对更改时组件状态的引用 正在应用。它不应该直接突变。相反,变化 应该以根据来自 状态和道具。
因此,从上面的摘录中我们可以推断出我们应该从当前的state
对象中构建一个新对象,以便对其进行更改和随意操作,并将其传递给setState()
更新state
。
由于我们正在处理嵌套对象,因此我们需要一种深度复制对象的方法,并假设您不想使用任何第三方库(lodash)来这样做,那么我想出的是:
this.setState( (state) => {
let newState = JSON.parse(JSON.stringify(state));
newState.inputs.username.touched = true;
return ({
inputs: newState.inputs
});
});
请注意,当您的state
具有嵌套对象时,您也不应使用let newState = Object.assign({},state)
。因为那样会浅复制state
嵌套对象引用,因此您仍然可以直接更改状态,因为newState.inputs === state.inputs === this.state.inputs
是正确的。它们都指向相同的对象inputs
。
但是,由于JSON.parse(JSON.stringify(obj))
有其性能限制,并且还有一些数据类型或循环数据可能不支持JSON,因此建议您采用什么其他方法深度复制嵌套对象以进行更新它吗?
我想出的另一个解决方案是:
this.setState( (state) => {
let usernameInput = {};
usernameInput['username'] = Object.assign({},state.inputs.username);
usernameInput.username.touched = true;
let newInputs = Object.assign({},state.inputs,usernameInput);
return({
inputs: newInputs
});
};
在第二种选择中,我要做的是从要更新的最里面的对象(在本例中为username
对象)创建一个新对象。而且我必须在键username
中获取这些值,这就是为什么我使用usernameInput['username']
的原因,因为稍后我会将其合并到newInputs
对象中。一切都使用Object.assign()
完成。
第二个选项已经变得更好performance results。至少好50%。
关于此主题还有其他想法吗?很抱歉这个问题很长,但我认为它很好地说明了这个问题。
编辑:我从以下答案中采用的解决方案:
我的TextInput组件onChange事件侦听器(我通过React Context为它提供服务):
onChange={this.context.onChange(this.props.name)}
我在表单组件中的onChange函数
onChange(inputName) {
return(
(event) => {
event.preventDefault();
const newValue = event.target.value;
this.setState( (prevState) => {
return({
inputs: {
...prevState.inputs,
[inputName]: {
...prevState.inputs[inputName],
value: newValue
}
}
});
});
}
);
}
答案 0 :(得分:3)
我可以想到其他一些方法来实现它。
解构每个嵌套元素,仅覆盖正确的元素:
this.setState(prevState => ({
inputs: {
...prevState.inputs,
username: {
...prevState.inputs.username,
touched: true
}
}
}))
使用解构运算符复制您的输入:
this.setState(prevState => {
const inputs = {...prevState.inputs};
inputs.username.touched = true;
return { inputs }
})
编辑
使用计算属性的第一个解决方案:
this.setState(prevState => ({
inputs: {
...prevState.inputs,
[field]: {
...prevState.inputs[field],
[action]: value
}
}
}))
答案 1 :(得分:1)
您可以尝试使用嵌套的Object.Assign
:
const newState = Object.assign({}, state, {
inputs: Object.assign({}, state.inputs, {
username: Object.assign({}, state.inputs.username, { touched: true }),
}),
});
};
您还可以使用传播运算符:
{
...state,
inputs: {
...state.inputs,
username: {
...state.inputs.username,
touched: true
}
}
这是更新嵌套属性并保持状态不变的正确方法。
答案 2 :(得分:1)
我制作了一个util函数,可以使用动态键更新嵌套状态。
function _recUpdateState(state, selector, newval) {
if (selector.length > 1) {
let field = selector.shift();
let subObject = {};
try {
//Select the subobject if it exists
subObject = { ..._recUpdateState(state[field], selector, newval) };
} catch {
//Create the subobject if it doesn't exist
subObject = {
..._recUpdateState(state, selector, newval)
};
}
return { ...state, [field]: subObject };
} else {
let updatedState = {};
updatedState[selector.shift()] = newval;
return { ...state, ...updatedState };
}
}
function updateState(state, selector, newval, autoAssign = true) {
let newState = _recUpdateState(state, selector, newval);
if (autoAssign) return Object.assign(state, newState);
return newState;
}
// Example
let initState = {
sub1: {
val1: "val1",
val2: "val2",
sub2: {
other: "other value",
testVal: null
}
}
}
console.log(initState)
updateState(initState, ["sub1", "sub2", "testVal"], "UPDATED_VALUE")
console.log(initState)
您传递状态以及键选择器列表和新值。
您还可以将autoAssign值设置为false,以返回一个对象,该对象是旧状态的副本,但具有新的更新字段-否则,autoAssign = true,则更新前一个状态。
最后,如果选择器序列未出现在对象中,则将创建一个对象以及具有这些键的所有嵌套对象。
答案 3 :(得分:-2)
使用点差运算符
let {foo} = this.state;
foo = {
...foo,
bar: baz
}
this.setState({
foo
})