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';
}
 | 
多个 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 进行性能优化
在类组件中,我们可以通过 componentDidUpdate 中 prevProps 和 prevState 的 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
参考