React中useState更新多层嵌套对象数组失效问题问询
兄弟,你这个问题我之前处理React嵌套状态更新时也踩过一模一样的坑!本质是浅拷贝和深拷贝的区别在搞鬼。
先给你拆解清楚:
你用{...locations}做的是浅拷贝——它只会复制locations顶层对象的键值对,但对于里面嵌套的数组(比如locations[locationLevel]),拷贝的只是数组的引用,而非数组里的所有元素。也就是说,tLocations[locationLevel]和原locations[locationLevel]指向的是同一个数组,你修改tLocations[locationLevel][locationIndex].state时,其实直接改动了原状态里的对象。
至于控制台显示的矛盾:这是浏览器控制台的“小陷阱”——它保存的是对象的引用,当你先打印整个tLocations,再修改属性,之后展开控制台对象时,它会读取最新的引用值,所以会出现“单个对象显示正确,但整个数组展开还是旧值”的错觉,核心原因就是引用没断开。
再对比你更新playerState的情况:你同样用了浅拷贝,但因为修改的是顶层对象直接包含的数组(discardDeck),之后把新的顶层对象传给setPlayerState,React检测到顶层对象的引用变了,就触发了重新渲染,所以界面看起来正常。但这种方式其实不符合React的不可变数据原则,原playerState里的discardDeck已经被你push修改了,只是暂时没暴露问题而已。
要彻底解决问题,你需要逐层拷贝要修改的嵌套层级,断开所有引用,保证状态的不可变性。推荐用函数式更新的方式(能拿到最新的prevState,更安全):
setLocations(prevLocations => { // 1. 拷贝顶层locations对象 const newLocations = {...prevLocations}; // 2. 拷贝要修改的那一层数组(locationLevel对应的数组) const updatedLevelArray = [...newLocations[locationLevel]]; // 3. 拷贝要修改的那个location对象 const updatedLocation = {...updatedLevelArray[locationIndex]}; // 4. 修改目标属性 updatedLocation.state = LOCATION_STATE.occupied; // 5. 逐层替换回去 updatedLevelArray[locationIndex] = updatedLocation; newLocations[locationLevel] = updatedLevelArray; // 6. 返回新的状态 return newLocations; });
如果你的嵌套结构更复杂,也可以用深拷贝工具(比如Lodash的_.cloneDeep),但要注意JSON.parse(JSON.stringify(locations))这种深拷贝方式会忽略函数、Symbol、undefined等特殊类型,若locations里有这些内容就不能用。
这样修改后,你再打印整个状态,就能看到对应的state已经正确变成occupied了,而且React也能准确检测到状态变化,避免后续渲染问题。
内容的提问来源于stack exchange,提问作者Faire

