- 尽管
React
官方文档给出了两条 Hook 规则,但无论是新手还是经验丰富的 React 开发人员,都常常会忘记遵循React Hooks
的规则。 - 因此,
React
团队开发了一个名为eslint-plugin-react-hooks
的ESLint
插件,以帮助开发人员在自己的项目中以正确的方式编写 React Hooks。 - 这个插件能够帮助我们在尝试运行应用程序之前捕获并修复 Hooks 错误,所以最好将此插件添加到我们有使用 React Hooks 的项目中。
- 需要注意的是,
eslint-plugin-react-hooks
插件约定,当在以大驼峰法命名的函数(视作一个组件)或在useSomething
函数(视作一个自定义 Hook)中调用Hooks 时,lint
规则才能正常地工作: - 如以下代码所示:
- 当我们在项目中使用了 eslint-plugin-react-hooks 插件后,会发生 eslint 报错,提示我们不能条件式调用 Hooks:
- 而当我们将组件改成匿名默认导出时, 可以看到,lint 规则并没有生效(相关issue):
- 所以在实际开发过程中,在写函数式组件时,要给组件进行大驼峰法命名;而在写自定义
Hook
时,也要遵循useSomething
的命名规则
-
当创建类组件时,遵循一定的顺序可以帮助我们更好地维护和改进 React 应用程序代码:
static
开头的类属性- 构造函数,
constructor
getter/setter
- 组件生命周期
_
开头的私有方法 事件监听方法,handle*
render*
开头的方法,有时候 render() 方法里面的内容会分开到不同函数里面进行,这些函数都以 render* 开头render()
方法
-
虽然不按上面顺序组织代码,组件功能也不会有什么影响。但是,如果所有的组件都按这种顺序来编写,那么维护起来就会方便很多,多人协作的时候别人理解代码也会一目了然。
-
对函数组件而言,并没有所谓的构造器和生命周期函数,只要编写的代码按照 Hook 规则实施,一般都不会遇到什么问题:
-
与类组件一样,虽然编写顺序对功能实现没有大的影响,但是,为函数组件创建定义的结构能够改善项目的可读性。
-
推荐在函数顶部使用
useState Hook
和useRef Hook
,然后使用useEffect Hook
编写订阅,接着编写与组件作业相关的其他函数,最后返回要由浏览器渲染的元素:
- 我们都知道,
useEffect
用来引入具有副作用的操作,最常见的就是向服务器请求数据。对 React Hook 不熟悉的小白可能会在没有使用Typescript
的项目中犯以下错误: - 当在跳转路由(离开当前界面)的时候会遇到如下错误:
- 而我们都知道,Typescript 能帮我们做静态类型检查,若在使用 Typescript 的项目的
useEffect
中错误使用了异步函数,编译器就会及时地报出下面的错误(而不至于在组件卸载时才触发报错):
Argument of type '() => Promise' is not assignable to parameter of type 'EffectCallback'.
- 发生这样错误的原因是使用异步函数导致我们在使用
useEffect()
的时候返回的是一个值而非函数。通常,effects
需要在组件销毁之前清除创建的资源,例如订阅或计时器ID。为此,传递给 useEffect的函数应该返回一个清理函数。原本需要返回的是一个cleanup
函数,而使用异步函数会使callback
返回Promise
而不是 cleanup 函数,因此当组件销毁时候,就会发生报错的情况。 - 为了避免直接将
Promise
作为effects
返回值,我们可以把异步函数独立出来: - 或使用IIFE( Immediately Invoked Function Expression)匿名函数表达式:
useEffect
和useLayoutEffect
,是两个工作方式很相似的React Hook
,它们二者的不同在于执行时机:- useEffect 是在渲染函数执行完成,并绘制到屏幕之后,再异步执行
- useLayoutEffect是在渲染函数执行之后,屏幕重绘前同步执行
- 因为
useLayoutEffect
是同步执行的,因此会发生阻塞,直到该effect
执行完成才会进行页面重绘,如果 effect 内部有执行很慢的代码,可能会引起性能问题。因此,React 官方指出,尽可能使用标准的 useEffect 以避免阻塞视觉更新。 useLayoutEffect
也不是毫无作用,下面介绍它的一个使用场景。- 当点击按钮时,会发现页面发生闪烁:
- 原因在于,
useEffect
的触发时机会被延迟到 DOM 绘制完成。因此我们点击按钮setState
后,其实经历了 设置高度为 50 -> 绘制屏幕 -> 设置高度为100 的两次 render 过程,所以肉眼才会看到中间过渡的 state 导致的闪烁的感觉。 - 而前面提到,
useLayoutEffect
会在所有 DOM 改变后,同步调用。在浏览器运行绘制之前,useLayoutEffect 内部的更新将被同步刷新。正因为这个 hook 的特性,我们可以使用它来让 DOM 的渲染慢一拍,等待 state 真正更新完后才去渲染浏览器的画面。 - 我们将 useEffect 改为 useLayoutEffect:
- 前面提到,我们可以使用
React.memo()
这个高阶组件来控制函数组件的重复渲染。 - 导致重复渲染的原因是
React Hooks
使用的是函数组件,父组件的任何一次修改,都会导致子组件的函数执行,从而重新进行渲染。 - 因此,为了性能方面考虑,除了使用
React.memo()
对函数组件进行包装,我们还可以使用 React 提供的useCallback
和useMemo
来对针对函数和函数的返回值进行缓存。 - 需要注意的是, useCallback 和 useMemo 要结合 React.memo() 才能避免子组件无效渲染。
-
在
React Hooks
出现以前,我们可以通过Render Props
和高阶组件(HOC)两种方式来实现 React 组件的状态逻辑共享。 -
而如今,得益于 Hooks 的逻辑封装能力,我们可以将常见的逻辑封装起来,以减少代码复杂度。
-
通过使用 React Hooks 后,我们可以把状态和逻辑关联起来,分拆成多个自定义 Hooks,代码结构就会变得更清晰:
-
比如现在有多个页面,都需要用到在 antd Modal 组件中嵌入自己的表单,同时可以由组件自己控制 Modal 的显示:
-
可以看到,通过将多个页面共有的逻辑封装在
useActionModal
中,可以大大减少代码量和维护成本。 -
需要强调的是,我们不能在类组件中使用 Hooks,所以如果项目中还有老式的类组件,需要使用自定义 Hooks,就需要将它们转换为函数式组件或者使用其他可重用逻辑模式(HOC 或Render Props),如可将 Hook 包装成 HOC:
-
使用的时候将我们的目标组件用上述的 withHooksHOC 包装起来,那么我们就可以将 width 属性传递给目标组件
-
由于官方自带的 Hooks 远远无法满足我们的开发需求 ,目前社区上别人也封装了一些自定义 Hooks 库,如 react-use 等,大家可以学习一下相关自定义 Hooks 的实现。
-
本条是针对
React + Redux
项目的实践建议,如果项目中使用的是Mobx
或其他状态管理库,可跳过本条实践。 -
React Redux
在 7.1 版本中提供了一组 Hooks 作为现有 connect() 高阶组件的替代方案:useSelector
:替换 connect() 的mapStateToProps
方法。它接受一个函数作为参数,该函数使用 Redux 的存储状态并返回所需的状态。useDispatch
:替换 connect() 的mapDispatchToProps
方法。它所做的只是返回 store 的dispatch
方法,因此我们也可以手动调用dispatch。
-
下面通过代码来对比两者写法不同:
-
可见,将
useSelector
和useDispatch
作为connect()
的替代方案,代码更干净,也显得更有条理。