最近在团队内部review代码的过程中,引发了关于受控组件设计的一些思考。
关于受控组件,在刚接触React开发的时候,就了解到了这个概念,大体的实现就是给组件提供一个value, 一个onChange,value 用于渲染当前的UI视图,在用户触发操作,
需要更新value的时候,组件调用onChange,由上层组件负责更新状态。整体设计的一个关键点就是:
受控组件内部不维护任何相关的状态,状态都由外部传入
对于状态的更新,提供onChange回调
组件的这种设计方式,对于对接方来说,有付出,也有收益,
付出,就是指顶层组件需要维护相关的状态,并定义状态更新的回调(其实这也算不上付出。。。)
收益,就是指组件的扩展性和灵活性提高了,对于组件来说,我给你什么,你就渲染什么,而且状态由顶层的组件维护,在任何的时刻,都可以监视到
当前的状态,对于一些需要扩展验证规则,以及其他特殊限制的情况,再适合不过。
(最近Beisen开发了一批新的组件,在对接过程中,已经体验过非受控组件的痛苦了)
所以,在提供一些模块,供其他人对接的情况下,尽量将组件设计为受控模式,并统一提供value与onChange回调。
当状态更新时,由组件内部处理状态,并传给onChagne回调。
这种设计,对于一些状态单一的组件来讲,是非常合适的。如果需要提供的是一个比较复杂的组件,有多个状态,比如 A B C 三个状态的情况,这时候,我们固然可以将
ABC三个状态都放到value中,并定义统一的onChange回调,接受更新后的状态。但是如果对于A的变化,外部需要做一些额外的逻辑,如果仅仅通过一个onChange来实现,
可能会增加无谓的负责度。所以在组件设计上,我们就可以扩展一些回调出来,比如onChangeA 等等,并且可以设计为可选参数。
下面来看一下,代码重构前与重构后的对接方式:
重构前:
重构后:
下面是review过程中发现的其他一些问题:
在组件的实现设计上,我抽象了view 与 数据层,数据只负责对元数据过滤,view负责渲染,然后写出了如下的代码:
<SCApp>
<DataParser metaData={metaData} render={renderView} />
</SCApp>
DataParser就是数据处理层,这里他接受了一个render方法。这个设计其实是非常不合理的,作为数据处理层,其实就是函数,作为函数,就有输入和输出。很明显,这的输入就是未加工的元数据,
输出就是 加工好的元数据。 他不需要关心UI视图的渲染结果,参数设计上,不应该接受render方法。
所以,这里将DataParse层作为一个元数据处理的hooks更加合理,具体重构后的代码可以见 项目
由于任务列表是一个支持展开收起,显示子列表的结构,所以在设计组件的状态数据时,我当时的思路是这样子的:
export interface IDataPoolItem {
metaData: {
bizData: IBizData;
row: Row;
};
childrenIds: string[];
hasChildren: boolean;
pid: string;
isOpen: boolean;
}
metaData 就是组件渲染需要的一些元数据信息,对于展开和收起,在每一个数据上都设计了一个isOpen字段,标识是否是展开和收起。
这样设计,错是没有错,功能也可以实现,但或许不是最合理的。
首先,我没发一眼看到当前所有展开的节点,二是,如果我需要将所有的节点收起,那就需要先遍历找出所有展开的节点,然后将其isOpen置为false。
光这两点,就能看出不合理的地方了。
在这里的状态设计上,没有遵守的一个原则就是,UI 与 业务数据分离。 节点的展开收起是一个UI状态,与其他的业务数据不同,我们需要对这种类型的数据做单独维护和处理,
所以,新开辟一个数组,存放所有展开/收起节点的ID,是再合适不过的了