设置对象的属性,嵌套在另一个对象中,直接将其设置在状态的解构副本上,这是一个不好的做法吗?



我发现了一篇文章,其中指出,如果我想改变属性name在这样的状态:

const [user, setUser] = useState({
name: 'Cody',
age: 25,
education: {
school: {
name: 'School of Code'
}
}
})

我需要做以下事情:

setUser(prevUser => {
return {
...prevUser,
education: {
...prevUser.education,
school : {
...prevUser.education.school,
name: 'Layercode Academy'
}
}
}
})

然而,他们后来表明,有可能使这个逻辑更简单,使用immerp .js(也改变useImmer上的useState),像这样:

setUser(draft => {
draft.education.school.name = 'Layercode Academy';
})

我的问题是我是否可以做到这一点,而不使用mer.js:

setUser(prevUser => {
const newUser = {...prevUser}
newUser.education.school.name = 'Layercode Academy'
return newUser
})

在我所见过的每个教程(不使用immerj .js)中,他们都做了解构。但是在很多情况下,给状态副本的属性赋值似乎更简单、更简洁。我没有直接设置state,只是修改copy,没有违反任何"规则"。。是否存在一些隐藏的陷阱?

这是两个副本之间的区别

浅拷贝

const a = { x: 0, y: { z: 0 } };
const b = {...a}; // or const b = Object.assign({}, a);
b.x = 1; // doesn't update a.x
b.y.z = 1; // also updates a.y.z

深拷贝

const a = { x: 0, y: { z: 0 } };
const b = JSON.parse(JSON.stringify(a)); 
b.y.z = 1; // doesn't update a.y.z

setState()不会立即改变它。状态,但会创建挂起的状态转换。访问。状态调用此方法后可能返回现有值。——丹

因此,考虑到这一点,您不必深度克隆所有内容,因为它可能非常昂贵并导致不必要的渲染。

所以如果你需要在渲染后处理数据

setUser(prevUser => {
return {
...prevUser,
education: {
...prevUser.education,
school : {
...prevUser.education.school,
name: 'Layercode Academy'
}
}
}
})
doSomething(user) // setUser merely schedules a state change, so your 'user' state may still have old value
}

相反,

setUser(prevUser => {
return {
...prevUser,
education: {
...prevUser.education,
school : {
...prevUser.education.school,
name: 'Layercode Academy'
}
}
}
}, (user) => { doSomething(user})

这样可以保证用户的更新,而不必担心突变。

是否有一些隐藏的陷阱?

是的,虽然我们不知道你的其他代码是否会遇到它们。所以一般来说就把它当作一个毯子"是的"。

你所做的是改变状态。如果整个操作只执行一次状态更新和重新呈现,则不太可能遇到问题。但它仍然建立在一个坏习惯的基础上。

在执行更多操作的情况下(多个批处理状态更新,在处理状态更新之前使用当前状态的其他逻辑等),那么您更有可能看到意想不到的(并且难以跟踪的)错误和行为。

"不改变状态"的总体规则这就是一个总的规则。确实存在一个可以改变状态而不会引起任何问题的情况。但是为什么呢?为什么仅仅因为问题还没有发生就依赖坏习惯呢?

状态更新应该是一个全新的对象(或数组,或只是普通值)。对于没有改变的对象属性,它仍然可以是对前一个对象属性的引用(解构会这样做)。但是对于任何改变了的属性,导致该属性的整个对象图都应该是新的。

是的,你的最后一个例子有问题。您仍然在修改原始用户的教育对象:

setUser(prevUser => {
const newUser = {...prevUser}
newUser.education.school.name = 'Layercode Academy'
// true
prevUser.education.school.name === newUser.education.school.name
return newUser;
});

您可以考虑将状态分解为多个状态(即原语):

const [userEducationSchoolName, setUserEducationSchoolName] = useState("School of Code");

但是很快就失控了。如果您的环境支持,您甚至可以使用structuredClone:

setUser(prevUser => {
const newUser = structuredClone(prevUser);
newUser.education.school.name = 'Layercode Academy'
return newUser
});

,但由于一些浏览器不支持structuredClone,你将需要一些其他的方式来深度克隆对象。这又把我们带回到伊默尔!Immer的全部意义在于为您提供状态的可变深层副本,供您更改。

最新更新