什么是React Hook?它到底香不香?

1,873 阅读7分钟
作者:Yukee

1. Hook 简介

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

1.1 大纲

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

1.2 简要概述

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

1.3 React为什么需要Hook呢

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

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

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

1.3.2 复杂组件变得难以理解

这里存在两种情况:

  1. 同一块功能的代码逻辑被拆分在了不同的生命周期函数中

  2. 一个生命周期中混杂了多个不相干的代码逻辑

二者均不利于我们维护和迭代。例如,组件常常在 componentDidMount 和 componentDidUpdate 中获取数据。还有,在 componentDidMount 中设置事件监听,而之后需在 componentWillUnmount 中清除等。同时在 componentDidMount 中也可能还有其他的功能逻辑,导致不便于理解代码。通过 React Hook 可以将功能代码聚合,方便阅读维护,我将在后面Effect Hook小节中对其进行举例说明*如何将功能分块,实现关注点分离。

1.3.3 其他便利好处

  1. 不用再考虑该使用无状态组件(Function)还是有状态组件(Class)而烦恼,使用hook后所有组件都将是Function

在 React 的世界中,有容器组件和 UI 组件之分,在 React Hooks 出现之前,UI组件我们可以使用函数,无状态组件来展示UI,而对于容器组件,函数组件就显得无能为力,我们依赖于类组件来获取数据,处理数据,并向下传递参数给UI组件进行渲染。

  1. 不用再纠结使用哪个生命周期钩子函数

  2. 不需要再面对this

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

import React, { useState } from 'react';

function Example() {
  // useState 有两个返回值分别为当前 state 及更新 state 的函数
  // 其中 count 和 setCount 分别与 this.state.count 和 this.setState 类似
  const [count, setCount] = useState(0);// 数组解构写法

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

export default Example;

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

class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Click me
        </button>
      </div>
    );
  }
}
export default Example;

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

import React, { useState } from "react";
function Example() {
  const [obj, setObject] = useState({
    count: 0,
    name: "ml"
  });
  return (
    <div>
      Count: {obj.count}
      <button onClick={() => setObject({ ...obj, count: obj.count + 1 })}>+</button>
    </div>
  );
}
export default Example;

3. Effect Hook使用

Effect Hook 可以让你在函数组件中执行副作用操作,可以将其当作 componentDidMount , componentDidUpdate 和 componentWillUnmount 这三个函数的组合,根据是否需要清除副作用可分为两种。

(副作用操作包括:异步请求,设置订阅以及手动更改 React 组件中的 DOM 等)

3.1 不需要清除副作用

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

import React, { useState, useEffect } from 'react';

function Example() {
  // 声明一个新的叫做 “count” 的 state 变量
  const [count, setCount] = useState(0);

  // 相当于 componentDidMount 和 componentDidUpdate,在挂载和更新时均会执行第一个参数的函数
  useEffect(() => {
    document.title = `You clicked ${count} times`;// 使用浏览器的 API 更新页面标题
  }, [count]);// 第二个参数作用为仅在 [count] 更改时更新,需要注意的是该参数为一个数组,只要有一个元素变化即会执行 effect

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

export default Example;

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

class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  componentDidMount() {
    document.title = `You clicked ${this.state.count} times`;
  }

  componentDidUpdate() {
    document.title = `You clicked ${this.state.count} times`;
  }

  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Click me
        </button>
      </div>
    );
  }
}
export default Example;

3.2 需要清除副作用

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

import React, { Component } from "react";
class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }
  componentDidMount() {
    const { count } = this.state;
    document.title = `You have waited ${count} seconds`;
    this.timer = setInterval(() => {
      this.setState(({ count }) => ({
        count: count + 1
      }));  // 友情提醒:可以理解一下这里 setState 的用法
    }, 1000);
  }
  componentDidUpdate() {
    const { count } = this.state;
    document.title = `You have waited ${count} seconds`;
  }
  componentWillUnmount() {
    document.title = "componentWillUnmount";
    clearInterval(this.timer);
  }
  render() {
    const { count } = this.state;
    return (
      <div>
        Count:{count}
        <button onClick={() => clearInterval(this.timer)}>clear</button>
      </div>
    );
  }
}
export default App;

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

import React, { useState, useEffect } from "react";
let timer = null;
function App() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    document.title = `You have waited ${count} seconds`;
  },[count]);

  useEffect(() => {
    timer = setInterval(() => {
      setCount(prevCount => prevCount + 1);
    }, 1000);
    // 返回值可类比 componentWillUnmount,返回的是清除函数
    // 返回值(如果有)则在组件销毁或者调用第一个参数中的函数前调用(后者也可理解为执行当前 effect 之前对上一个对应的 effect 进行清除)
    return () => {
      document.title = "componentWillUnmount";
      clearInterval(timer);
    };
  }, []); // 空数组代表第二个参数不变,即仅在挂载和卸载时执行 
  return (
    <div>
      Count: {count}
      <button onClick={() => clearInterval(timer)}>clear</button>
    </div>
  );
}
export default App;

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

Hook 允许我们按照代码的用途分离他们, 而不是像生命周期函数那样。React 将按照 effect 声明的顺序依次调用组件中的每一个 effect。

下面是 React 官网的例子,较好的说明了将组件中不同功能的代码分块,实现关注点分离,便于维护管理。

function FriendStatusWithCounter(props) {
  const [count, setCount] = useState(0);

  // 更新标题
  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

  const [isOnline, setIsOnline] = useState(null);

  // 订阅好友状态
  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }

    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });
  // ...
}

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

class FriendStatus extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0, isOnline: null };
    this.handleStatusChange = this.handleStatusChange.bind(this);
  }

  componentDidMount() {
    document.title = `You clicked ${this.state.count} times`;
    ChatAPI.subscribeToFriendStatus(this.props.friend.id, this.handleStatusChange);
  }

  componentDidUpdate(prevProps) {
    document.title = `You clicked ${this.state.count} times`;
    // 取消订阅之前的 friend.id
    ChatAPI.unsubscribeFromFriendStatus(prevProps.friend.id, this.handleStatusChange);
    // 订阅新的 friend.id
    ChatAPI.subscribeToFriendStatus(this.props.friend.id, this.handleStatusChange);
  }

  componentWillUnmount() {
    ChatAPI.unsubscribeFromFriendStatus(this.props.friend.id, this.handleStatusChange);
  }

  handleStatusChange(status) {
    this.setState({
      isOnline: status.isOnline
    });
  }
  // ...
}

4. 自定义 Hook

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

import { useState, useEffect } from 'react'

export default function (initialName = '') {
  const [name, setName] = useState(initialName)
  const [isValid, setIsValid] = useState(false)
  const [message, setMessage] = useState(undefined)

  useEffect(() => {
    if (name.length >= 2 && name.length <= 5) {
      setIsValid(true)
      setMessage(undefined)
    } else {
      setIsValid(false)
      setMessage('输入的名字不可以低于2位或超过5位')
    }
  }, [name])

  let result = {
    value: name,
    isValid,
    message
  }

  return [result, setName]
}

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

import React from 'react'
import useName from './hook/useName'

export default function () {
  let [name, setName] = useName('')

  return (
    <div>
      <input value={name.value} onChange={(e) => {setName(e.target.value)}} />
      {!name.isValid && <p>{name.message}</p>}
    </div>
  )
}

同样,该自定义的姓名校验Hook可以在项目其他需要姓名校验的地方使用来实现复用。

可以较为明显看出,Hook能够有效地复用状态逻辑,相较于高阶组件和Render props来实现复用状态逻辑,Hook没有嵌套过深的问题,使用起来比较简便清晰。

5. Hook 规则

  1. 只能在最顶层使用 Hook

不要在循环,条件或嵌套函数中调用 Hook

function Form() {
  const [name, setName] = useState('Mary');

  useEffect(function updateTitle() {
    document.title = name;
  });

  // 不合规则的错误方式
  if (name !== '') {
    useEffect(function persistForm() {
      localStorage.setItem('formData', name);
    });
  }

  // 修正上条错误的方式
  useEffect(function persistForm() {
    // 如需条件判断,应将条件判断放置在 effect 中
    if (name !== '') {
      localStorage.setItem('formData', name);
    }
  });
}
  1. 只在 React 函数中调用 Hook

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

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

6. 结语

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