
作者:苏木团队:增长中心
前言:react hooks被越来越多的人认可,整个社区都以积极的态度去拥抱它。在最近的一段时间笔者也开始在一些项目中尝试去使用react hooks。原本以为react hooks很简单,和类组件差不多,看看api就能用起来了。结果在使用中遇到了各种各样的坑,通过阅读react hooks相关的文章发现react hooks和类组件有很多不同。由此,想和大家做一些分享。
如果要在项目中使用react hooks,强烈推荐先安装<code>eslint-plugin-react-hooks</code>(由react官方发布)。在很多时候,这个eslint插件在我们使用react hooks的过程中,会帮我们避免很多问题。
本文主要讲以下内容:
函数式组件和类组件的不同
react hooks依赖数组的工作方式
如何在react hooks中获取数据
一、函数式组件和类组件的不同react hooks由于是函数式组件,在异步操作或者使用usecallback、useeffect、usememo等api时会形成闭包。
先看一下以下例子。在点击了<code>展示现在的值</code>按钮三秒后,会alert点击次数:
我们按照下面的步骤去操作:
点击<code>num</code>到3
点击<code>展示现在的值</code>按钮
在定时器回调触发之前,点击增加<code>num</code>到5。
可以猜一下alert会弹出什么?
分割线
其最后弹出的数据是3。
为什么会出现这样的情况,最后的<code>num</code>不是应该是5吗?
上面例子中,<code>num</code>仅是一个数字而已。它不是神奇的“data binding”, “watcher”, “proxy”,或者其他任何东西。它就是一个普通的数字像下面这个一样:
我们组件第一次渲染的时候,从<code>usestate()</code>拿到<code>num</code>的初始值为0,当我们调用<code>setnum(1)</code>,react会再次渲染组件,这一次<code>num</code>是1。如此等等:
在我们更新状态之后,react会重新渲染组件。每一次渲染都能拿到独立的<code>num</code>状态,这个状态值是函数中的一个常量。
所以在<code>num</code>为3时,我们点击了<code>展示现在的值</code>按钮,就相当于:
即便num的值被点击到了5。但是触发点击事件时,捕获到的<code>num</code>值为3。
上面的功能,我们尝试用类组件实现一遍:
我们按照之前同样的步骤去操作:
在定时器回调触发之前,点击增加<code>num</code>到5
这一次弹出的数据是5。为什么同样的例子在类组件会有这样的表现呢?我们可以仔细看一下handleclick方法:
这个类方法从this.state.num中读取数据,在react中state是不可变的。然而,this是可变的。通过类组件的<code>this</code>,我们可以获取到最新的state和props。所以如果在用户再点击了<code>展示现在的值</code>按钮的情况下我们对<code>点击</code>按钮又点击了几次,<code>this.state</code>将会改变。<code>handleclick</code>方法从一个“过于新”的<code>state</code>中得到了<code>num</code>。
这样就引起了一个问题,如果说我们ui在概念上是当前应用状态的一个函数,那么事件处理程序和视觉输出都应该是渲染结果的一部分。我们的事件处理程序应该有一个特定的props和state。然而在类组件中,我们通过<code>this.state</code>读取的数据并不能保证其是一个特定的state。<code>handleclick</code>事件处理程序并没有与任何一个特定的渲染绑定在一起。从上面的例子,我们可以看出react hooks在某一个特定渲染中state和props是与其相绑定的,然而类组件并不是。
二、react hooks依赖数组的工作方式在react hooks提供的很多api都有遵循依赖数组的工作方式,比如usecallback、useeffect、usememo等等。使用了这类api,其传入的函数、数据等等都会被缓存。被缓存的内容其依赖的props、state等值就像上面的例子一样都是“不变”的。只有当依赖数组中的依赖发生变化,它才会被重新创建,得到最新的props、state。所以在用这类api时我们要特别注意,在依赖数组内一定要填入依赖的props、state等值。
这里给大家举一个反例:
<code>usecallback</code>本质上是添加了一层依赖检查。当我们函数本身只在需要的时候才改变。在上面的例子中,我们无论点击多少次<code>点击</code>按钮,<code>num</code>的值始终为1。这是因为<code>usecallback</code>中的函数被缓存了,其依赖数组为空数组,传入其中的函数会被一直缓存。<code>handleclick</code>其实一直都是:
即便函数再次更新,<code>num</code>的值变为1,但是react并不知道你的函数中依赖了<code>num</code>,需要去更新函数。
唯有在依赖数组中传入了<code>num</code>,react才会知道你依赖了<code>num</code>,在<code>num</code>的值改变时,需要更新函数。
点击<code>点击</code>按钮,num的值不断增加。
(其实这些归根究底,就是react hooks会形成闭包)
三、如何在react hooks中获取数据在我们用习惯了类组件模式,我们在用react hooks中获取数据时,一般刚开始大家都会这么写吧:
其实这样是不推荐的一种模式,要记住effect外部的函数使用了哪些props和state很难。这也是为什么通常你会想要在effect内部去声明它所需要的函数。这样就能容易的看出那个effect依赖了组件作用域中的哪些值:
但是如果你在不止一个地方用到了这个函数或者别的原因,你无法把一个函数移动到effect内部,还有一些其他办法:
如果这函数不依赖state、props内部的变量。可以把这个函数移动到你的组件之外。这样就不用其出现在依赖列表中了。
如果其不依赖state、props。但是依赖内部变量,可以将其在effect之外调用它,并让effect依赖于它的返回值。
万不得已的情况下,你可以把函数加入effect的依赖项,但把它的定义包裹进<code>usecallback</code>。这就确保了它不随渲染而改变,除非它自身的依赖发生了改变。
另外一方面,业务一旦变的复杂,在react hooks中用类组件那种方式获取数据也会有别的问题。我们做这样一个假设,一个请求入参依赖于两个状态分别是query和id。然而id的值需要异步获取(只要获取一次,就可以在这个组件卸载之前一直用),query的值从props传入:
在这里,当我们的依赖的<code>query</code>在异步获取<code>id</code>期间变了,最后请求的入参,其<code>query</code>将会用之前的值。(引起这个问题的原因还是闭包,这里就不再复述了)
对于从后端获取数据,我们应该用react hooks的方式去获取。这是一种关注数据流和同步思维的方式。对于刚才这个例子,我们可以这样解决:
一方面这种方式可以让我们的代码更加清晰,一眼就能看明白获取这个接口的数据依赖了哪些state、props,让我们更多的去关注数据流的改变。另外一方面也避免了闭包可能会引起的问题。但是同步思维的方式也会有一些坑,比如这样的场景,有一个列表,这个列表可以通过子元素的按钮增加数据:
这种场景下,会一直加载数据,造成死循环。每次调用<code>fetchdata</code>函数会更新<code>list</code>,<code>list</code>更新后<code>fetchdata</code>函数就会被更新。<code>fetchdata</code>更新后<code>useeffect</code>会被调用,<code>useeffect</code>中又调用了<code>fetchdata</code>函数。<code>fetchdata</code>被调用导致<code>list</code>更新...当出现这种根据前一个状态更新状态的时候,我们可以用usereducer去替换usestate:
react会保证<code>dispatch</code>在组件的声明周期内保持不变。所以上面的例子中不需要依赖<code>dispatch</code>。
用了<code>usereducer</code>我们就可以移除<code>list</code>依赖。不会再出现死循环的情况。通过dispatch了一个action来描述发生了什么。这使得我们的<code>fetchdata</code>函数和<code>list</code>状态解耦。我们的<code>fetchdata</code>函数不再关心怎么更新状态,它只负责告诉我们发生了什么。更新的逻辑全都交由reducer去统一处理。
(我们使用函数式更新也能解决这个问题,但是更推荐使用usereducer)在某些场景下<code>usereducer</code>会比usestate更适用。例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的state等。并且,使用 <code>usereducer</code> 还能给那些会触发深更新的组件做性能优化,因为你可以向子组件传递 <code>dispatch</code> 而不是回调函数。
如果大家遇到其它的一些复杂场景,用上面介绍的方法无法解决。那就试试用useref吧。文章如有疏漏、错误欢迎批评指正。