如果你之前用过 class,你或许会试图总是在一次 useState() 调用中传入一个包含了所有 state 的对象。如果你愿意的话你可以这么做。这里有一个跟踪鼠标移动的组件的例子。我们在本地 state 中记录它的位置和尺寸:
function Box() {
const [state, setState] = useState({ left: 0, top: 0, width: 100, height: 100 });
// ...
}
现在假设我们想要编写一些逻辑以便在用户移动鼠标时改变 left 和 top。注意到我们是如何必须手动把这些字段合并到之前的 state 对象的:
// ...
useEffect(() => {
function handleWindowMouseMove(e) {
// 展开 「...state」 以确保我们没有 「丢失」 width 和 height
setState(state => ({ ...state, left: e.pageX, top: e.pageY }));
}
// 注意:这是个简化版的实现
window.addEventListener('mousemove', handleWindowMouseMove);
return () => window.removeEventListener('mousemove', handleWindowMouseMove);
}, []);
// ...
这是因为当我们更新一个 state 变量,我们会 替换 它的值。这和 class 中的 this.setState 不一样,后者会把更新后的字段 合并 入对象中。
如果你还怀念自动合并,你可以写一个自定义的 useLegacyState Hook 来合并对象 state 的更新。
import { useState, useEffect } from "react";
function useLegacyState(initialState) {
const [state, setState] = useState(initialState);
useEffect(() => {
setState(prevState => {
return { ...prevState, ...state };
});
}, [state]);
return [state, setState];
}
然而,我们推荐把 state 切分成多个 state 变量,每个变量包含的不同值会在同时发生变化。
举个例子,我们可以把组件的 state 拆分为 position 和 size 两个对象,并永远以非合并的方式去替换 position:
function Box() {
const [position, setPosition] = useState({ left: 0, top: 0 });
const [size, setSize] = useState({ width: 100, height: 100 });
useEffect(() => {
function handleWindowMouseMove(e) {
setPosition({ left: e.pageX, top: e.pageY });
}
// ...
把独立的 state 变量拆分开还有另外的好处。这使得后期把一些相关的逻辑抽取到一个自定义 Hook 变得容易,比如说:
function Box() {
const position = useWindowPosition();
const [size, setSize] = useState({ width: 100, height: 100 });
// ...
}
function useWindowPosition() {
const [position, setPosition] = useState({ left: 0, top: 0 });
useEffect(() => {
// ...
}, []);
return position;
}
注意看我们是如何做到不改动代码就把对 position 这个 state 变量的 useState 调用和相关的 effect 移动到一个自定义 Hook 的。如果所有的 state 都存在同一个对象中,想要抽取出来就比较难了。
把所有 state 都放在同一个 useState 调用中,或是每一个字段都对应一个 useState 调用,这两方式都能跑通。当你在这两个极端之间找到平衡,然后把相关 state 组合到几个独立的 state 变量时,组件就会更加的可读。如果 state 的逻辑开始变得复杂,我们推荐 用 reducer 来管理它,或使用自定义 Hook。