一、Hook 简介

在官网的描述是这样的:Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。

1. 大纲

本文将主要针对开发React Hook的动机,即为什么需要它进行讲述,同样这也是React Hook的价值,并对最常用的两个Hook函数(State Hook和Effect Hook)的使用进行详细举例说明。

2. 简要概述

Hook 是一个特殊的函数,它可以让你 “钩入” React 的特性,包括state、生命周期等。例如,useState 是允许你在 React 函数组件中添加 state 的 Hook。

3. React为什么需要Hook呢

看客姥爷们如果觉得第一遍看完以下原因不好直接理解,建议可以简单过一下本节,看完后续小节后重新回顾一遍本节噢~

3.1 在组件间复用状态逻辑很难

在原本的代码中,我们经常使用高阶组件(HOC)/render props等方式来复用组件的状态逻辑,无疑增加了组件树层数及渲染,使得代码难以理解,而在 React Hook 中,这些功能都可以通过强大的自定义的 Hook 来实现,我将在后面自定义Hook小节中对其进行举例详细说明。

3.2 复杂组件变得难以理解

这里存在两种情况:
1. 同一块功能的代码逻辑被拆分在了不同的生命周期函数中
2. 一个生命周期中混杂了多个不相干的代码逻辑
二者均不利于我们维护和迭代。例如,组件常常在 componentDidMountcomponentDidUpdate 中获取数据。还有,在 componentDidMount 中设置事件监听,而之后需在 componentWillUnmount 中清除等。同时在 componentDidMount 中也可能还有其他的功能逻辑,导致不便于理解代码。通过 React Hook 可以将功能代码聚合,方便阅读维护,我将在后面Effect Hook小节中对其进行举例说明*如何将功能分块,实现关注点分离。

3.3 其他便利好处

1. 不用再考虑该使用无状态组件(Function)还是有状态组件(Class)而烦恼,使用hook后所有组件都将是Function
在 React 的世界中,有容器组件和 UI 组件之分,在 React Hooks 出现之前,UI组件我们可以使用函数,无状态组件来展示UI,而对于容器组件,函数组件就显得无能为力,我们依赖于类组件来获取数据,处理数据,并向下传递参数给UI组件进行渲染。
2. 不用再纠结使用哪个生命周期钩子函数
3. 不需要再面对this
**

二、State Hook使用

它让我们在 React 函数组件上添加内部 state,以下是一个计数器例子:

  1. import React, { useState } from 'react';
  2. function Example() {
  3. // useState 有两个返回值分别为当前 state 及更新 state 的函数
  4. // 其中 count 和 setCount 分别与 this.state.count 和 this.setState 类似
  5. const [count, setCount] = useState(0);// 数组解构写法
  6. return (
  7. <div>
  8. <p>You clicked {count} times</p>
  9. <button onClick={() => setCount(count + 1)}>
  10. Click me
  11. </button>
  12. </div>
  13. );
  14. }
  15. export default Example;

原来类组件的等价功能是如何实现呢?是不是稍微简洁了些呢?

  1. class Example extends React.Component {
  2. constructor(props) {
  3. super(props);
  4. this.state = {
  5. count: 0
  6. };
  7. }
  8. render() {
  9. return (
  10. <div>
  11. <p>You clicked {this.state.count} times</p>
  12. <button onClick={() => this.setState({ count: this.state.count + 1 })}>
  13. Click me
  14. </button>
  15. </div>
  16. );
  17. }
  18. }
  19. export default Example;import React, { useState } from 'react';
  20. function Example() {
  21. // useState 有两个返回值分别为当前 state 及更新 state 的函数
  22. // 其中 count 和 setCount 分别与 this.state.count 和 this.setState 类似
  23. const [count, setCount] = useState(0);// 数组解构写法
  24. return (
  25. <div>
  26. <p>You clicked {count} times</p>
  27. <button onClick={() => setCount(count + 1)}>
  28. Click me
  29. </button>
  30. </div>
  31. );
  32. }
  33. export default Example;

需要注意的是 useState 不帮助你处理状态,相较于 setState 非覆盖式更新状态,useState 覆盖式更新状态,需要开发者自己处理逻辑,如下所示:

  1. import React, { useState } from "react";
  2. function Example() {
  3. const [obj, setObject] = useState({
  4. count: 0,
  5. name: "ml"
  6. });
  7. return (
  8. <div>
  9. Count: {obj.count}
  10. <button onClick={() => setObject({ ...obj, count: obj.count + 1 })}>+</button>
  11. </div>
  12. );
  13. }
  14. export default Example;

三、Effect Hook使用

Effect Hook 可以让你在函数组件中执行副作用操作,可以将其当作 componentDidMountcomponentDidUpdatecomponentWillUnmount 这三个函数的组合,根据是否需要清除副作用可分为两种。
(副作用操作包括:异步请求,设置订阅以及手动更改 React 组件中的 DOM 等)

3.1 不需要清除副作用

即在更新 DOM 之后运行的一些额外的代码,包括异步请求、手动更改 DOM 等,以下为手动更改 React 组件中的 DOM 的一个例子:

  1. import React, { useState, useEffect } from 'react';
  2. function Example() {
  3. // 声明一个新的叫做 "count" 的 state 变量
  4. const [count, setCount] = useState(0);
  5. // 相当于 componentDidMount 和 componentDidUpdate,在挂载和更新时均会执行第一个参数的函数
  6. useEffect(() => {
  7. document.title = `You clicked ${count} times`;// 使用浏览器的 API 更新页面标题
  8. }, [count]);// 第二个参数作用为仅在 [count] 更改时更新,需要注意的是该参数为一个数组,只要有一个元素变化即会执行 effect
  9. return (
  10. <div>
  11. <p>You clicked {count} times</p>
  12. <button onClick={() => setCount(count + 1)}>
  13. Click me
  14. </button>
  15. </div>
  16. );
  17. }
  18. export default Example;

原来类组件的等价功能如何实现呢?是不是稍微简洁了些呢?

  1. class Example extends React.Component {
  2. constructor(props) {
  3. super(props);
  4. this.state = {
  5. count: 0
  6. };
  7. }
  8. componentDidMount() {
  9. document.title = `You clicked ${this.state.count} times`;
  10. }
  11. componentDidUpdate() {
  12. document.title = `You clicked ${this.state.count} times`;
  13. }
  14. render() {
  15. return (
  16. <div>
  17. <p>You clicked {this.state.count} times</p>
  18. <button onClick={() => this.setState({ count: this.state.count + 1 })}>
  19. Click me
  20. </button>
  21. </div>
  22. );
  23. }
  24. }
  25. export default Example;

3.2 需要清除副作用

有时为了防止内存泄漏,需要清除一些副作用,以下为一个定时计数器例子,这节先看看原来类组件应该如何实现:

  1. import React, { Component } from "react";
  2. class App extends Component {
  3. constructor(props) {
  4. super(props);
  5. this.state = {
  6. count: 0
  7. };
  8. }
  9. componentDidMount() {
  10. const { count } = this.state;
  11. document.title = `You have waited ${count} seconds`;
  12. this.timer = setInterval(() => {
  13. this.setState(({ count }) => ({
  14. count: count + 1
  15. })); // 友情提醒:可以理解一下这里 setState 的用法
  16. }, 1000);
  17. }
  18. componentDidUpdate() {
  19. const { count } = this.state;
  20. document.title = `You have waited ${count} seconds`;
  21. }
  22. componentWillUnmount() {
  23. document.title = "componentWillUnmount";
  24. clearInterval(this.timer);
  25. }
  26. render() {
  27. const { count } = this.state;
  28. return (
  29. <div>
  30. Count:{count}
  31. <button onClick={() => clearInterval(this.timer)}>clear</button>
  32. </div>
  33. );
  34. }
  35. }
  36. export default App;

以上例子需要结合多个生命周期才能完成功能,还有重复的编写,来看看 useEffect 的写法:

  1. import React, { useState, useEffect } from "react";
  2. let timer = null;
  3. function App() {
  4. const [count, setCount] = useState(0);
  5. useEffect(() => {
  6. document.title = `You have waited ${count} seconds`;
  7. },[count]);
  8. useEffect(() => {
  9. timer = setInterval(() => {
  10. setCount(prevCount => prevCount + 1);
  11. }, 1000);
  12. // 返回值可类比 componentWillUnmount,返回的是清除函数
  13. // 返回值(如果有)则在组件销毁或者调用第一个参数中的函数前调用(后者也可理解为执行当前 effect 之前对上一个对应的 effect 进行清除)
  14. return () => {
  15. document.title = "componentWillUnmount";
  16. clearInterval(timer);
  17. };
  18. }, []); // 空数组代表第二个参数不变,即仅在挂载和卸载时执行
  19. return (
  20. <div>
  21. Count: {count}
  22. <button onClick={() => clearInterval(timer)}>clear</button>
  23. </div>
  24. );
  25. }
  26. export default App;

3.3 使用多个 Effect 实现关注点分离

Hook 允许我们按照代码的用途分离他们, 而不是像生命周期函数那样。React 将按照 effect 声明的顺序依次调用组件中的每一个 effect。
下面是 React 官网的例子,较好的说明了将组件中不同功能的代码分块,实现关注点分离,便于维护管理。

  1. function FriendStatusWithCounter(props) {
  2. const [count, setCount] = useState(0);
  3. // 更新标题
  4. useEffect(() => {
  5. document.title = `You clicked ${count} times`;
  6. });
  7. const [isOnline, setIsOnline] = useState(null);
  8. // 订阅好友状态
  9. useEffect(() => {
  10. function handleStatusChange(status) {
  11. setIsOnline(status.isOnline);
  12. }
  13. ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
  14. return () => {
  15. ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
  16. };
  17. });
  18. // ...
  19. }

而同样功能在类组件中则需要较为麻烦的写法,并且不便于理解,如下所示:

  1. class FriendStatus extends React.Component {
  2. constructor(props) {
  3. super(props);
  4. this.state = { count: 0, isOnline: null };
  5. this.handleStatusChange = this.handleStatusChange.bind(this);
  6. }
  7. componentDidMount() {
  8. document.title = `You clicked ${this.state.count} times`;
  9. ChatAPI.subscribeToFriendStatus(this.props.friend.id, this.handleStatusChange);
  10. }
  11. componentDidUpdate(prevProps) {
  12. document.title = `You clicked ${this.state.count} times`;
  13. // 取消订阅之前的 friend.id
  14. ChatAPI.unsubscribeFromFriendStatus(prevProps.friend.id, this.handleStatusChange);
  15. // 订阅新的 friend.id
  16. ChatAPI.subscribeToFriendStatus(this.props.friend.id, this.handleStatusChange);
  17. }
  18. componentWillUnmount() {
  19. ChatAPI.unsubscribeFromFriendStatus(this.props.friend.id, this.handleStatusChange);
  20. }
  21. handleStatusChange(status) {
  22. this.setState({
  23. isOnline: status.isOnline
  24. });
  25. }
  26. // ...
  27. }

四、自定义 Hook

这里从一个简单的表单验证入手来了解自定义Hook的方法及复用状态逻辑,首先是一个自定义的姓名校验Hook

  1. import { useState, useEffect } from 'react'
  2. export default function useName(initialName = '') {
  3. const [name, setName] = useState(initialName)
  4. const [isValid, setIsValid] = useState(false)
  5. const [message, setMessage] = useState(undefined)
  6. useEffect(() => {
  7. if (name.length >= 2 && name.length <= 5) {
  8. setIsValid(true)
  9. setMessage(undefined)
  10. } else {
  11. setIsValid(false)
  12. setMessage('输入的名字不可以低于2位或超过5位')
  13. }
  14. }, [name])
  15. let result = {
  16. value: name,
  17. isValid,
  18. message
  19. }
  20. return [result, setName]
  21. }

我们再来看看,在组件中如何使用这个自定义的Hook来完成姓名验证

  1. import React from 'react'
  2. import useName from './hook/useName'
  3. export default function Example() {
  4. let [name, setName] = useName('')
  5. return (
  6. <div>
  7. <input value={name.value} onChange={(e) => {setName(e.target.value)}} />
  8. {!name.isValid && <p>{name.message}</p>}
  9. </div>
  10. )
  11. }

同样,该自定义的姓名校验Hook可以在项目其他需要姓名校验的地方使用来实现复用。
可以较为明显看出,Hook能够有效地复用状态逻辑,相较于高阶组件和Render props来实现复用状态逻辑,Hook没有嵌套过深的问题,使用起来比较简便清晰。

五、 Hook 规则

1. 只能在最顶层使用 Hook
不要在循环,条件或嵌套函数中调用 Hook

  1. function Form() {
  2. const [name, setName] = useState('Mary');
  3. useEffect(function updateTitle() {
  4. document.title = name;
  5. });
  6. // 不合规则的错误方式
  7. if (name !== '') {
  8. useEffect(function persistForm() {
  9. localStorage.setItem('formData', name);
  10. });
  11. }
  12. // 修正上条错误的方式
  13. useEffect(function persistForm() {
  14. // 如需条件判断,应将条件判断放置在 effect 中
  15. if (name !== '') {
  16. localStorage.setItem('formData', name);
  17. }
  18. });
  19. }

2. 只在 React 函数中调用 Hook
不要在普通的 JavaScript 函数中调用 Hook,可以:

  • 在 React 的函数组件中调用 Hook
  • 在自定义 Hook 中调用其他 Hook

六、结语

个人认为 Hook 最大的两个亮点便是状态逻辑的复用和关注点分离的思想,同样其他写法上的优化点也是可圈可点的,大佬们怎么看呢?恳请批评指正 biubiubiu~
**

七、参考

30分钟精通React Hooks
React Hooks原理
React Hooks原理(源码解析)
精读《Hooks 取数 - swr 源码》