事情的起因是这样的,团队内部技术分享下周将轮到我,我准备分享一下hooks的使用。
技术分享这个事情,对于我来说,是展示逼格儿的一个东西,所以我想在分享的过程中尽量使用一些比较官方&标准的话术来表达。于是我就去了(应该是几个月没有去过的)React官网。
我虽然明白Hooks的好处在哪里,但是还是想看看官网是如何描述的,准备用来复述,然后看到了这么一句话:
复杂组件变得难以理解
我们经常维护一些组件,组件起初很简单,但是逐渐会被状态逻辑和副作用充斥。每个生命周期常常包含一些不相关的逻辑。例如,组件常常在
componentDidMount和componentDidUpdate中获取数据。但是,同一个componentDidMount中可能也包含很多其它的逻辑,如设置事件监听,而之后需在componentWillUnmount中清除。相互关联且需要对照修改的代码被进行了拆分,而完全不相关的代码却在同一个方法中组合在一起。如此很容易产生 bug,并且导致逻辑不一致。
在多数情况下,不可能将组件拆分为更小的粒度,因为状态逻辑无处不在。这也给测试带来了一定挑战。同时,这也是很多人将 React 与状态管理库结合使用的原因之一。但是,这往往会引入了很多抽象概念,需要你在不同的文件之间来回切换,使得复用变得更加困难。
为了解决这个问题,Hook 将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据) ,而并非强制按照生命周期划分。你还可以使用 reducer 来管理组件的内部状态,使其更加可预测。
以上
在 componentDidMount 和 componentDidUpdate 中获取数据。但是,同一个 componentDidMount 中可能也包含很多其它的逻辑,如设置事件监听,而之后需在 componentWillUnmount 中清除。相互关联且需要对照修改的代码被进行了拆分,而完全不相关的代码却在同一个方法中组合在一起。如此很容易产生 bug,并且导致逻辑不一致。
这一段话让我思考了一小会,因为我之前从没有意识到过这样写有什么问题,随之想起了设计模式原则之一的单一职责原则,也就是实现类要职责单一,只做一件事。虽然一个生命周期or函数远比类的概念要小很多,但是原则也不是单单针对类而言。在一个componentDidMount中:
componentDidMount = () => {
this.addEvent(); // 增加原生事件
this.startInterval(); // 开启计时器
this.tableDataFetch(); // 获取接口数据
}
我们做了3件毫不相干的事情,如果用追求完美的眼光来看待,这样是不合格的。当然,这不是业务要求的问题,也不是开发者书写的问题,而是类组件限制必须这么写的问题,所以说这是Hooks比class优秀的原因之一,虽然以前我只是单纯的认为Hooks最大的好处是将可以一些公共的逻辑提炼出来复用。
当然,我不止是只想了这么多。
因为我们的项目都需要写单元测试,而Hooks之前一直没有被学习和使用的一个原因是大家都不知道单元测试怎么写,既然我决定了分享Hooks,那么单元测试必定也是我的任务之一。
在费了不是很大但也不少的周折之后,终于搞懂了Hooks的单元测试怎么写。当然,也正是因为这个,才让我真正意识到了Hooks的另一个好处。
在调查Hooks的单元测试之前,我一般情况下写hooks都是这样写的。
const FcSeedToBrainContainer = () => {
const [data, setData] = useState([]);
const { f1 } = useXXX() // 公共hooks;
const { f2 } = useXXX() // 又一个公共hooks
useEffect(() => {
f3()
}, [id]);
const f3 = useCallback(async () => {
// 一些逻辑balabal
const data = await fn(id)
setData(data)
}, [id])
return (
<div>一些DOM</div>
)
}
当然这么写是没有任何问题的,但是写单元测试我发现,func Com内的方法不太好进行测试,那怎么办呢?只能一个个提出来了,也就是,将之提到组件外部作为一个外部Hooks。
用代码表示就是
const useGetData = (id) => {
const [data, setData] = useState([]);
useEffect(() => {
f3()
}, [id]);
const f3 = useCallback(() => {
// 一些逻辑balabal
const data = await fn(id)
setData(data)
}, [id])
return { data, setData, f3 };
}
const FcSeedToBrainContainer = () => {
const { f1 } = useXXX() // 公共hooks;
const { f2 } = useXXX() // 有一个公共hooks
const { data } = useGetData(id);
return (
<div>111</div>
)
}
虽然useGetData是这个组件独有的hooks,但是也将之提取到外部,这样在性能上和写到内部没有什么区别。但是有一个最大的好处就是
将单一职责的Hooks的单独提取出来,进行了逻辑分离,保持了组件内部的纯洁,出了BUG又可以根据Hooks的职责快速定位出位置;如果逻辑可以进行复用,那么又避免了重新提取所消耗的精力
于是乎我接下来的几天业务代码都是将每一个职责的hooks单独提取,突然发现,Hooks写的再也不乱了,哪怕对Hooks一知半解的同事,也能维护我写的组件了。
两个简单的小想法,记录在掘金~!