理解 React 组件中的 State

最近我发现,开发组件的时候,有的同学倾向于新增状态,而不是使用纯函数 + memoize 的方式解决问题,这一定程度上滥用了 state,到最后可能自己也会困惑,不清楚何时要根据属性变化重新同步内部状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 反模式

const Item = ({ type, list }) => {
const [listType, setListType] = useState(type);
const [itemList, setItemList] = useState(list);

useEffect(() => {
// transform by type
setItemList(transformedList);
}, []);

return (
<Fragment>
{
itemList.map(item => <Item {..item}/>)
}
</Fragment>
)
}

定义内部状态

我们抛开具体框架,组件的状态本质上是组件的内部变量,你可以定义任意多的变量。但会面临一个问题:很多变量是由其他变量通过计算衍生而来,顶层变量的改变,可能会触发整个组件的生命周期变更,并导致内部状态的大量更新。此时,如何对变量之间进行数据同步,是一个不小的问题。

什么时候组件必然会新增一个状态?

  • 组件内部自治的状态,且不传递依赖于父组件的状态以及任何其他状态(不能被现有状态衍生而来),比如非受控表单的值
  • 受限于实现的场景,比如异步接口请求、异步事件等(实际上更类似一个临时变量暂存其结果)

除了以上情况,其他的变量都可以通过函数计算而衍生出,所以理论上完全不新增任何变量是可行的。而函数内部定义了状态之间的依赖和变换,状态之间是天然同步的,免去了手动更新的问题。

1
2
3
4
5
6
7
8
9
render() {
return (
<Fragment>
{
transform(list, type).map(item => <Item {..item}/>)
}
</Fragment>
)
}

但很多时候我们还是倾向于使用新的状态而不是函数调用,一方面是因为在高频访问状态的时候,完全依赖函数调用不如直接访问状态来的优雅和高效,另一方面是其存在性能问题(虽然可以增加一层 memoize 来优化)。所以或多或少,我们会定义一些新的中间状态,这时就不得不考虑状态同步问题。

一点经验

我的建议是:

对外部状态进行依赖管理,例如 React 可以通过生命周期函数(componentDidUpdate)监听父组件属性改变,并完整地同步组件内的状态,而 Vue 可以通过 computed 保持数据同步;注意数据之间必须完整地被同步,只要其他状态对其有计算依赖,都应该被重新更新,否则必然会导致异常;简单粗暴的方式是一旦关键状态改变,重新初始化整个组件,这个视具体情况实现

1
2
3
4
5
6
7
8
9
componentDidUpdate(prevProps, prevState, snapshot) {
if (prop1Changed) {
this.updateProp1();
}
}

updateProp1() {
// update prop2
}

更多地使用纯函数计算来进行消费,辅以缓存优化,减少数据同步工作,特别是那些纯展示数据,以及没有被频繁依赖的数据

1
2
3
4
5
6
7
8
9
10
11
const Item = ({ type, list }) => {
const itemList = useMemo(() => transformList(list, type), [type, list]);

return (
<Fragment>
{
itemList.map(item => <Item {..item}/>)
}
</Fragment>
)
}

题外话

这个时候我们可以看出 Hooks 的一些好处:

首先是对副作用的聚合,对于每一个数据的同步和变换的因素都在 Dependency List 中被声明,连析构动作也被包含在内,这比 Class Component 放在单独的生命周期函数中更易于维护和突出,不易被忽略。同时,一系列的变换都是自上而下的,不会散落各处,符合编程直觉。

自定义 Hooks 让独立逻辑的抽离变得更加容易,开发者也更愿意主动去拆分和复用。

所以大家可以更多地去尝试 Hooks。