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。