Featured image of post React Hooks

React Hooks

React Hooks 简介

Hook 含义

组件类和函数组件

useState

使用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import React, { useState } from 'react';

const Example() {
  // 声明了一个 count 的 state 变量,并初始化为 0
  // setCount 设置 state 值 count 的方法
  const [count, setCount] = useState(0);
  
  // 每次点击按钮,将 count 的值 +1
  return (
    <>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </>
  )
}

export default Example;

语法

1
2
// 声明一个 state 变量,并同时初始化
const [state, setState] = useState(initialState)
  • useState 返回一个包含两个元素的数组
    • state 变量,指向状态当前值
    • setState 更新 state 值的方法
  • initialState 状态初始值可以是数字,字符串,数组,对象等

与类中使用 setState 异同

  • 相同点
    • 在一次渲染周期中调用多次 setState,数据只改变一次
  • 不同点
    • 类组件中 setState 为合并
    • 函数组件中 setState 为替换

useEffect

useEffect 用来执行副作用。可以将 useEffect Hook 看做 componentDidMount, componentDidUpdate, componentWillMount 生命周期函数的组合。

常用在:

  • 服务器请求
  • 访问元素 dom 元素
  • 本地持久化缓存
  • 绑定/解绑事件
  • 添加订阅
  • 设置定时器
  • 记录日志

useEffect 接收一个函数,该函数会在组件渲染完毕后才执行,该函数有要求:要么返回一个能清除副作用的函数,要么就不返回任何内容。

不需要清除的 effect

在 React 更新 DOM 之后,我们想运行一些额外的代码。比如发送网络请求,手动变更 DOM,记录日志,这些都是常见的无需清除的操作。 因为我们在执行完这些操作之后,就可以忽略他们了。

以下为使用 class 和 Hook 都是怎么实现这些副作用的对比。

使用类组件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 在 React 更新 DOM 操作后,立即更新 document 的 title 属性
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() 不能有任何副作用。应该在 react 更新 DOM 后再执行副作用。
  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times.</p>
        <button onClick={() => this.setState({ count: this.state.count + 1})}>
          click me.
        </button>
      <div/>
    )
  }
}

使用 Hook

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import React, {useState, useEffect} from 'react';

export default function Example() {
  const [count, setCount] = useState(0);
  useEffect(() => {   // 经过渲染或更新, DOM 更新完毕之后,会运行该 effect
    document.title = `You click ${count} times`;
  });

  return (
    <div>
      <p>You clicked {this.state.count} times.</p>
      <button onClick={() => this.setState({ count: this.state.count + 1})}>
        click me.
      </button>
    <div/>
  );
}
  • 第一次渲染之后和每次更新之后都会执行 useEffect。且 React 保证每次运行 effect 的同时,DOM 已经更新完毕。
  • useEffect 放在组件内部让我们可以在 effect 中直接访问 state / props。我们不需要特殊的 API 来读取。
  • 与 componentDidMount 或 componentDidUpdate 不同,使用 useEffect 调度的 effect 不会阻塞浏览器更新屏幕,这让你的应用看起来响应更快。大多数情况下,effect 不需要同步地执行。在个别情况下(例如测量布局),有单独的 useLayoutEffect Hook 供你使用,其 API 与 useEffect 相同。

需要清除的 effect

有一些副作用是需要清除的,如订阅外部数据源。这种情况下,清除工作是非常重要的,可以防止引起内存泄露!

使用类组件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class FriendStatus extends React.Component {
  constructor(props) {
    super(props);
    this.state = { isOnline: null };
    this.handleStatusChange = this.handleStatusChange.bind(this);
  }

  componentDidMount() {
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }
  componentDidUpdate () {
    // ...
  }
  componentWillUnmount() {
    // 在组件卸载时,关闭订阅的数据源
    ChatAPI.unsubscribeFromFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

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

  render() {
    if (this.state.isOnline === null) {
      return 'Loading...';
    }
    return this.state.isOnline ? 'Online' : 'Offline';
  }
}

使用函数组件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import React, {useState, useEffect} from 'react';

export default function FriendStatus(props) {
  const [isOnline, setIsOnline] = useState(null);
  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }

    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    // effect 可选清除副作用的函数,返回函数不一定需要命名
    return function cleanup() {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}
  • React 会在组件卸载的时候执行清除操作。

多个 Effect 实现关注点分离

使用 Hook 的其中一个目的是要解决: class 生命周期函数中经常包含不相关逻辑,相关逻辑又分离在几个不同的方法中。

如下为上文中计数器和好友状态指示器逻辑组合在一起的组件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class FriendStatusWithCounter extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0,
      isOnline: null
    };
    this.handleStautsChange = this.handleStatusChange.bind(this);
  }

  // 相应的逻辑被分配在三个不同的生命周期函数中
  componentDidMount() {
    // counter
    document.title = `You clicked ${this.state.count} times`;
    // friend status
    ChatAPI.subscribeToFriendStatus(
      this.props.fried.id,
      this.handleStatusChange
    )
  }
  componentDidUpdate() {
    document.title = `You clicked ${this.state.count} times`;
  }
  componentWillUnmount() {
    ChatAPI.unsubscribeFromFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

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

  // ...
}

若使用 Hook,和使用多个 state 的 Hook 一样,通过使用多个 effect,将不相关的逻辑分离到不同的 effect 中。

Hook 允许按照代码的用途进行分离,不同于生命周期函数。将按照 effect 声明的顺序依次调用组件中的每一个 effect。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
export default const FriendStatusWithCounter = () => {
  const [count, setCount] = useState(0);
  useEffect(() => {
    document.title = `You clicked ${this.state.count} times`;
  });

  const [isOneline, useIsOneline] = useState(null);
  useEffect(() => {
    function handleStatusChange(status) {
      setIsOneline(status.isOneline);
    }

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

  // ...
}

跳过 Effect 进行性能优化

在类组件中,我们可以通过 componentDidUpdateprevPropsprevState 的 props / state 的变化比较,判断是否需要执行某些副作用。

1
2
3
4
5
componentDidUpdate(prevProps, prevState) {
  if (prevState.count !== this.state.count) {
    document.title = `You clicked ${this.state.count} times`;
  }
}

在 Hook 中,可以通过对 useEffect 传递数组作为第二可选参数,通知 React 跳过对 effect 的调用。

1
2
3
4
5
useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]);    // 表示 count 更改时会进行更新

// {... , [5]}  当 count === 5 时,跳过 effect
  • 若只想运行一次 effect(仅在组件挂载和卸载时执行),可传递一个空数组 [] 作为第二参数。
    • 传入一个空数组([]),effect 内部的 props 和 state 会一直拥有其初始值。
  • 使用该优化,请确保数组中包含了所有外部作用域中会随时间变化且在 effect 中使用的变量,否则代码会用到先前渲染中的旧变量。

每次更新的时候都要运行 Effect 的原因

自定义 Hook

自定义 Hook 可以将组件逻辑提取到可重用的函数中。

目前为止,在 React 中有两种流行的方式共享组件之间的状态逻辑:

  • render props
  • 高阶组件 使用 Hook 可以在不增加组件的情况下解决相同的问题。

创建自定义 Hook

当我们想在函数间共享逻辑时,我们可以把它提取到另外一个函数中。由于组件和 Hook 都是函数,所以也同样使用于这种方式。

自定义 Hook 是一个函数,其名称以 “use” 开头,函数内部可以调用其他的 Hook。

以下是根据上文中 FriendStatus 实例,所提取的自定义 Hook。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import { useState, useEffect } from 'react';

// 名称一定以 use 开头
function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null);

  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOneline);
    }

    ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
    return () => {
      ChatAPI.unsubcribeFromFriendStatus(friendID, handleStatusChange);
    };
  });

  return isOnline;
}

export default useFriendStatus;

在这个自定义 Hook 中,没有包含任何新内容(逻辑与组件中的完全一致)

使用自定义 Hook

与组件一致,请确保只在自定义 Hook 的顶层无条件地调用其他 Hook。

1
2
3
4
5
6
7
8
function FriendStatus(props) {
  const isOnline = useFriendStatus(props.friend.id);

  if (isOnline === null) {
    return "Loading...";
  }
  return isOnline ? 'Online' : 'Offline';
}

这段代码与之前的工作方式完全一样,我们只是将函数中需要共享的逻辑提取到单独的函数中。

  • 自定义 Hook 是一种自然遵循 Hook 设计的约定,不是 React 的特性。
  • 自定义 Hook 必须以 “use” 开头
  • 不同组件中使用相同的 Hook 不会共享 state。
    • 自定义 Hook 是重用状态逻辑的机制(例如设置为订阅并存储当前值),每次使用自定义 Hook 时,其中的所有 state 和副作用完全隔离。

在多个 Hook 之间传递信息

由于 Hook 本身就是函数,因此我们可以在它们之间传递信息。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 这是一个聊天消息接收者的选择器,它会显示当前选定的好友是否在线
function ChatRecipientPicker() {
  // 当前选择的好友 ID 保存在 recipientID 状态变量中
  const [recipientID, setRecipientID] = useState(1);
  // 当更新 recipientID 状态变量时,useFriendStatus Hook 会取消订阅之前选中的好友,并订阅新选中的好友状态
  const isRecipientOnline = useFriendStatus(recipientID);

  return (
    <>
      <Circle color={isRecipientOnline ? 'green' : 'red'} />
      <select  // <select> 中选择其他好友时更新 recipientID
        value={recipientID} 
        onChange={e => setRecipientID(Number(e.target.value))}
      >
        {
          friendList.map(friend => (
            <option key={friend.id} value={friend.id}>
              {friend.name}
            </option>
          ))
        }
      </select>
    </>
  )
}

我们将当前选择的好友 ID 保存在 recipientID 状态变量中,并会在用户从 <select> 中选择其他好友时更新这个 state。

由于 useState 会提供 recipientID 状态变量的最新值,可以将它作为参数传递给自定义的 useFriendStatus Hook。当我们选择不同的好友并更新 recipientID 状态变量时,useFriendStatus Hook 会取消订阅之前选中的好友,并订阅新选中的好友状态。

Hook 规则

额外的 Hook

参考

Licensed under CC BY-NC-SA 4.0