介绍 React Hooks

260 阅读4分钟

这是我参与11月更文挑战的第19天,活动详情查看:2021最后一次更文挑战

Hooks是React 16.8新增的功能。Hooks允许我们使用状态和其他React特性,而无需编写Class组件。React Hooks 要解决的问题是状态共享,是继 render-props 和 higher-order components 之后的第三种状态逻辑复用方案,不会产生 JSX 嵌套地狱问题。

import { useState } from 'react';

function Example() {
  // Declare a new state variable, which we'll call "count"
  const [count, setCount] = useState(0);

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

新的函数 useState 是一个我们学到的第一个 "Hook" ,但是这个例子只是个简单的例子

动机

Hooks 解决了在这五年里写和维护成百上千的 React 组件的时候,遇到的各种各样的互相不连接的问题。无论你什么时候开始学习 React, 并且日常都用它,或者甚至用一个不同的库但是是相似模型的组件,你都可能会遇到下面的这些问题

在不同的组件之间很难重用状态逻辑

react 不会提供一个重用组件的功能,(比如连接到 store)。如果你用 react 有一段时间了,你会熟悉 render props 和 高阶组件这些模式去解决这个问题。 但是使用这些模式需要你重构代码,会很麻烦而且代码很难追踪。 如果看了经典的 React 应用在 react DevTools 里面,你会找到组件的 “wrapper 黑洞” ,组件被包裹在 provider 层,消费者层,高阶组件, render props和其他抽象概念里面。React 需要一个更原生的写法去共享状态逻辑。

用 Hooks, 我们可以把状态逻辑抽象出来,独立测试和重用。Hooks 让你可以重用状态逻辑,而不需要对组件大改。这让你在很多组件和社区之间很容易分享 hooks

下面,我们来看一个hooks的例子

一个 Hooks 演变

我们先假想一个常见的需求,一个 Modal 里需要展示一些信息,这些信息需要通过 API 获取且跟 Modal 强业务相关,要求我们:

  • 因为业务简单,没有引入额外状态管理库
  • 因为业务强相关,并不想把数据跟组件分开放
  • API 数据会随机变动,因此需要每次打开 Modal 才获取最新数据
  • 为了后期优化,不可以有额外的组件创建和销毁

我们可能的实现如下:

class RandomUserModal extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      user: {},
      loading: false,
    };
    this.fetchData = this.fetchData.bind(this);
  }

  componentDidMount() {
    if (this.props.visible) {
      this.fetchData();
    }
  }

  componentDidUpdate(prevProps) {
    if (!prevProps.visible && this.props.visible) {
      this.fetchData();
    }
  }

  fetchData() {
    this.setState({ loading: true });
    fetch('https://randomuser.me/api/')
      .then(res => res.json())
      .then(json => this.setState({
        user: json.results[0],
        loading: false,
      }));
  }

  render() {
    const user = this.state.user;
    return (
      <ReactModal
        isOpen={this.props.visible}
      >
        <button onClick={this.props.handleCloseModal}>Close Modal</button>
        {this.state.loading ?
          <div>loading...</div>
          :
          <ul>
            <li>Name: {`${(user.name || {}).first} ${(user.name || {}).last}`}</li>
            <li>Gender: {user.gender}</li>
            <li>Phone: {user.phone}</li>
          </ul>
        }
      </ReactModal>
    )
  }
}

我们抽象了一个包含业务逻辑的 RandomUserModal,该 Modal 的展示与否由父组件控制,因此会传入参数 visible 和 handleCloseModal(用于 Modal 关闭自己)。

为了实现在 Modal 打开的时候才进行数据获取,我们需要同时在 componentDidMount 和 componentDidUpdate 两个生命周期里实现数据获取的逻辑,而且 constructor 里的一些初始化操作也少不了。

其实我们的要求很简单:在合适的时候通过 API 获取新的信息,这就是我们抽象出来的一个业务逻辑,为了这个业务逻辑能在 React 里正确工作,我们需要将其按照 React 组件生命周期进行拆解。这种拆解除了代码冗余,还很难复用

下面我们看看采用 Hooks 改造后会是什么样:

function RandomUserModal(props) {
  const [user, setUser] = React.useState({});
  const [loading, setLoading] = React.useState(false);

  React.useEffect(() => {
    if (!props.visible) return;
    setLoading(true);
    fetch('https://randomuser.me/api/').then(res => res.json()).then(json => {
      setUser(json.results[0]);
      setLoading(false);
    });
  }, [props.visible]);
  
  return (
    // View 部分几乎与上面相同
  );
}

很明显地可以看到我们把 Class 形式变成了 Function 形式,使用了两个 State Hook 进行数据管理(类比 constructor),之前 cDM 和 cDU 两个生命周期里干的事我们直接在一个 Effect Hook 里做了这些,最大的优势是代码精简,业务逻辑变的紧凑,代码行数也从 50+ 行减少到 30+ 行。

Hooks 的强大之处还不仅仅是这个,最重要的是这些业务逻辑可以随意地的的抽离出去,跟普通的函数没什么区别(仅仅是看起来没区别),于是就变成了可以复用的自定义 Hook。具体可以看下面的进一步改造:

// 自定义 Hook
function useFetchUser(visible) {
  const [user, setUser] = React.useState({});
  const [loading, setLoading] = React.useState(false);
  
  React.useEffect(() => {
    if (!visible) return;
    setLoading(true);
    fetch('https://randomuser.me/api/').then(res => res.json()).then(json => {
      setUser(json.results[0]);
      setLoading(false);
    });
  }, [visible]);
  return { user, loading };
}

function RandomUserModal(props) {
  const { user, loading } = useFetchUser(props.visible);
  
  return (
    // 与上面相同
  );
}

这里的 useFetchUser 为自定义 Hook,它的地位跟自带的 useState 等比也没什么区别,你可以在其它组件里使用,甚至在这个组件里使用两次,它们会天然地隔离开。