React Hooks开发实战
上QQ阅读APP看书,第一时间看更新

1.1 React Hooks概述

Hooks是React官方团队在React 16.8版本中正式引入的概念。通俗地讲,Hooks只是一些函数,Hooks可以用于在函数组件中引入状态管理和生命周期方法。如果希望让React函数组件拥有状态管理和生命周期方法,我们不需要再去将React函数组件重构成React类组件,而可以直接使用React Hooks。

值得注意的是,与原来React Class的组件不同,React Hooks在类中是不起作用的。React不仅提供了一些内置的Hooks,比如useState,还支持自定义Hooks,用于管理重复组件之间的状态。

1.1.1 React Hooks的优点

React Hooks具有以下优点。

1.简洁

为了更好地体现React Hooks的简洁,我们分别使用React Class的方式和React Hooks的方式来实现一个点击按钮切换字体颜色的小功能。

使用React Class的方式的语法实现如下。

上述代码通过在state中设置一个颜色状态属性来存储div内字体的颜色,当点击按钮的时候,取反设置color状态的值即可实现我们期望的功能。

下面来看看,同样的功能使用React Hooks的方式是怎样实现的。

从这个小例子中,你感受到React Hooks的简单之处了吗?相比React Class,使用React Hooks的方式不仅代码量几乎减半,而且代码的可读性也更胜一筹。

上面两段代码实现的功能如图1-1所示。

图1-1 改变颜色的示例

2.上手非常简单

笔者第一次看React Class教程时被吓到了,感觉上手太难,但由于React是前端从业者必须掌握的技能,所以只能啃下这块硬骨头。

React Class上手难表现为如下几点。

□生命周期函数难以理解,很难熟练掌握。

□与Redux状态管理相关的概念太多。

□高阶组件(HOC)很难理解。

和React Class相比,React Hooks的出现让React的学习成本降低了很多。具体体现在如下几点。

□基于函数式编程理念,门槛比较低,只需要掌握一些JavaScript基础知识。

□与生命周期相关的知识不用学,React Hooks使用全新的理念来管理组件的运作过程。

□与HOC相关的知识不用学,React Hooks能够完美解决HOC想要解决的问题,并且更可靠。

□MobX取代了Redux来做状态管理。

3.代码可复用性更好

如果在大型项目中用React,你会发现项目中的很多React组件冗长且难以复用,尤其是那些写成Class的组件,它们本身包含状态(state),使得对它们的复用十分麻烦。

虽然HOC也能解决这个问题,但是它会让你的组件变得非常复杂。如果你的项目过大或者业务复杂,HOC会导致你的组件难以理解和维护。

在实际项目中,React Hooks可以帮助复用一个有状态的组件,而且比较简单。为了更好地体现React Hooks的代码可复用性好,我们分别使用React Class方式和React Hooks方式以复用组件的方式来实现一个用户信息展示组件。

使用React Class实现组件的复用需要借助HOC。HOC的使用方法不在本书范围内,此处不做介绍,大家可以自行查阅相关资料。通过React Class的方式实现组件复用的具体步骤如下。

(1)定义一个HOC。

这里我们定义了一个HOC——withCounter,它用于获取组件的业务数据。

(2)编写一个展示性的普通组件。

这里我们编写了一个普通组件,该组件作为展示性组件,获取从HOC中传递过来的自定义userName属性,然后将该属性作为展示数据进行处理。

以上模式看上去挺不错,而且有很多库运用了这种模式,但我们仔细观察会发现,它会增加代码的层级关系,而且这种层级嵌套的方式可读性不好。如果组件非常复杂,那么整个组件的可读性就会非常差,比如aCom(bCom(cCom)),你甚至不知道里面到底发生了什么。

使用React Hooks方式实现组件复用的具体步骤如下。

(1)自定义Hooks钩子。

(2)使用钩子。

上述代码通过useEffect钩子获取数据,因为React Hooks是函数模式,所以这里直接返回外部需要的数据,最终在外部组件中导入这些数据并展示。

由以上可知,使用React Hooks的方式比使用React Class的方式更容易理解,而且更重要的是React Hooks具有很好的可复用性,因为React Hooks是函数的形式,其他地方需要使用直接导入即可。

4.与TypeScript结合更简单

我们几乎不用关注React Hooks组件与TypeScript的具体结合方法,这是Class组件不具备的。下面我们使用TypeScript语言来编写一个简单的用户信息展示组件UserInfo。

React Class+TypeScript版本如下。

React Hooks+TypeScript版本如下。

从上面的代码可以更加直观地看出,比起Class语法this指向的迷惑性,Hooks的函数式语法让代码看起来更为直观与简洁,各段业务逻辑天然隔离,这让代码维护性变得更好。

5.总结

最后,总结一下React Hooks的几个优点,具体如下。

声明一个简单的组件只要几行简单的代码

容易上手。对于初学者来说,相比复杂的Class的声明周期,Hooks的钩子函数更好理解。

简化业务。充分利用组件化的思想把业务拆分成多个组件,采用函数式编程风格、函数式组件,状态保存在运行环境中,每个功能都包裹在函数中,整体风格更清爽、更优雅。

方便数据管理。相当于两种提升:各个组件不用使用非常复杂的props多层传输就实现了解耦;向prop或状态取值更加方便,函数组件都从当前作用域直接获取变量,而Class组件需要先访问实例this,再访问其属性或者方法。

便于重构。因为减少了很多模板代码,组件,特别是小组件写起来更加省事。人们更愿意去拆分组件。而组件粒度越细,被复用的可能性越大。这样,Hooks也在不知不觉中改变人们的开发习惯,提高项目的组件复用率。

1.1.2 React Hooks的缺点

任何产品都不可能是完美的,React Hooks相比React Class也有自己的不足之处。

1.状态不同步问题

React Hooks是函数的形式,而函数的运行是独立的,每个函数都有一个独立的作用域。函数的变量保存在运行时的作用域里,当我们有异步操作的时候,经常会碰到异步回调的变量引用的是之前的,值没有更新。这其实就是我们常说的闭包问题。

下面看一个具体的示例。

上述代码首先声明了number变量,用来保存按钮点击的次数,然后设置了两个按钮:一个按钮立即执行点击,执行number++;而另一个按钮延时执行点击,延时两秒后执行number++。

运行上述代码,先点击“延时执行”按钮,然后点击“立即执行”按钮,最终输出结果如图1-2所示。输出的值并不是我们想要的。

图1-2 状态不同步示例

本来我们希望控制台输出的结果为10,但控制台真正输出的值却是旧值0。如果使用React Class实现这个功能,显然输出的值是我们希望的10。

造成这种结果的原因是,函数的变量保存在运行时的作用域里,在点击“延时执行”按钮的时候,按钮内执行函数的内部作用域将变量number复制了一份,因此它的值就还是之前的旧值0。这就是React Hooks状态不同步的问题。

当然,这个问题能够通过useRef钩子来解决。useRef将在后文中专门介绍,这里暂不展开。

2.useEffect依赖问题

有时候,某个useEffect依赖某个函数的不可变性(不会被改变的变量),这个函数的不可变性又依赖另一个函数的不可变性,这样便形成了一条依赖链。一旦这条依赖链的某个节点被意外改变,那么所有useEffect就会被意外触发。如果某个组件中依赖层级很多,那么就会造成严重的性能问题。

解决这个问题的方案就是,尽量减少useEffect的依赖项,让useEffect的每个依赖项都是明确的,保持它的单一职责性,增强组件的颗粒化。

下面通过实现一个常见的列表页面来展示useEffect依赖项颗粒化的必要性。

不推荐如下做法。

在上述代码中,页面根据location页面路由的变化更新页面的导航,然后监听页面输入框中的内容和分页table的页码,从而调用对应的更新函数。

推荐的做法如下。

在上述代码中,把useEffect依赖项根据业务类型进行了拆分,这样不仅可以确保它们仅用于一种效果,从而防止发生意外的错误,而且可以让代码更加直观和更好维护。

如果大家对useEffect的使用还有疑惑,可以暂时跳过此部分,后文有对useEffect更加具体的介绍。

1.1.3 使用React Hooks时的注意事项

要想用好React Hooks,需要注意以下事项。

1.自定义的React Hooks要遵循一些命名规范

自定义React Hooks的命名一律使用use作为前缀,形如useXXX,例如userUserInfo。虽然这仅是一种编码习惯,但是为了代码的可维护性,我们必须遵守这个规范。

2.仅在最外层调用React Hooks

不要在循环、条件和嵌套函数内调用React Hooks。当你想有条件地使用某些React Hooks时,请在这些Hooks中写入条件。这样能够确保每次组件呈现时都以相同的顺序调用React Hooks,避免出现一些不可确定的bug。

为了帮助大家理解,下面使用React Hooks实现一个根据登录信息设置登录状态的组件。

不推荐如下写法。

上述代码中,组件会根据userName中是否存在值,来在localStorage中设置一个状态,用于区分是否已经登录。这里直接在函数体内根据userName的值判断是否执行useEffect钩子函数,会导致useEffect钩子每次的执行与否都是根据userName的值来决定的,从而使得每次当前组件的渲染顺序是不确定的,这样,如果存在其他的业务逻辑,就很容易出现bug。

推荐的写法如下。

3.仅从React函数中调用React Hooks

不要从常规JavaScript函数中调用React Hooks,只在自定义React Hooks或者React组件中调用React Hooks,这不仅能够确保组件中的所有状态逻辑都清晰可见,还能够避免出现一些不可确定的bug。