React Hook

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

Hook 是为了解决以下问题:

  • 组件之间复用状态逻辑很难
  • 复杂组件变得难以理解
  • 难以理解的 class

使用 Hook 可以在无需修改组件结构的情况下复用状态逻辑

Hook 将组件中相互关联的部分拆分成更小的函数,而不是按照生命周期划分

Hook 可以在不使用 class 的情况下使用更多的 React 特性

State Hook

在使用 State Hook 之前要先引入 useState:

import React, { useState } from 'react'

React 官网有一个计数器的例子:

function calcNum() {
  // 使用解构赋值
  const [count, setCount] = useState(0)
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  )
}

在 Hook 之前,用 function 写的组件都是无状态组件,只有一个 render 功能,无法拥有自己的状态值,而有了 hook,就可以通过 useState 来实现 this.setState 功能

useState 接收一个值作为一个 state 的初始值,返回一个数组。这个数组有两个成员组成,第一个是状态的当前值,第二个是改变这个状态值的方法,就是一个专属的 setState

在使用 this.setState 的时候,必须要传入一个对象,而 useState 不需要这么做

与 this.setState 不同的是,useState 不会将新的 state 与旧的 state 合并

可以用如下几种方法调用 useState:

function ExampleWithManyStates() {
  // 声明多个 state 变量!
  const [age, setAge] = useState(42)
  const [fruit, setFruit] = useState('banana')
  const [todos, setTodos] = useState([{ text: 'Learn Hooks' }])
  // ...
}

useState 只接收一个初始值,可以为数字,字符串,数组,对象等等

Effect Hook

Effect Hook 可以让你在函数组件中执行副作用操作

主要的副作用操作如下:

  • 数据获取
  • 设置定义
  • 手动更改 React 组件中的 DOM
  • 其他

可以将 Effect Hook 看作是 componentDidMount,componentDidUpdate 和 componentWillUnmount 这三个函数的组合

在组件内部我们可以在 useEffect 中直接访问 state 变量或是 props,不需要通过 this.state.属性名或者 this.props.属性名访问

React 官网有这样的例子:

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

function Example() {
  const [count, setCount] = useState(0)
  useEffect(() => {
    document.title = `You clicked ${count} times`
  })
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  )
}

首先引入了 useState 和 useEffect,然后添加了一个状态 count,初始值为 0。

在 useEffect 中传入了一个箭头函数,函数内对 DOM 进行了操作,将 title 变成点击次数

每次 render 都会执行 useEffect,这是默认的行为,注意:useEffect 在 render 函数执行之后才会执行,而不是发生在 mount 或 update 之后,React 保证 DOM 在运行的时候就已经更新了副作用

副作用分为两种:

  • 需要清除的
  • 不需要清除的

不需要清除的 Effect

像发送网络请求,手动变更 DOM,记录日志,这些操作都是不需要清除的操作,因为执行完就可以忽略它们了

需要清除的 Effect

假如说你在 useEffect 中涉及到了订阅操作,那么就需要在 componentWillUnmount 中清除这个订阅操作,否则会导致内存泄漏

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
    )
  }

  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'
  }
}

这是 React 官网上的一个例子,FriendStatus 类在 componentDidMount 钩子上订阅了 this.props.friend.id,将 id 和 handleStatusChange 传入 ChatAPI.unsubscribeFromFriendStatus 中来获取好友的在线状态,然后在 componentWillUnmount 中清除了这个订阅

上面的例子转换成 Hook 如下:

function FriendStatus(props) {
  const [isOnline, setIsOnline] = useState(null)

  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline)
    }
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange)
    return function cleanup() {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange)
    }
  })

  if (isOnline === null) return 'Loading...'

  return isOnline ? 'Online' : 'Offline'
}

可见 Hook 相对于 class 形式的组件要简洁很多,因为 useEffect 将 componentDidMount,componentDidUpdate 和 componentWillUnmount 这三个函数融合在了一起

useEffect 返回的函数就是 componentWillUnmount 中要执行的函数,这是 useEffect 函数的可清除机制,每个 useEffect 都可以返回一个清除函数,可以将添加和移除订阅的代码放到一起

Hook 注意事项

只在 React 函数最顶层使用 Hook

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

可以安装 eslint-plugin-react-hooks 来强制执行这条规则

npm install eslint-plugin-react-hooks --save-dev

当然我们可以在单个组件中使用多个 State Hook 或 Effect Hook

只要 Hook 的调用顺序在每次渲染中都是相同的,React 就可以知道哪个 state 对应哪个 useState

这也是为什么不能使用 if 或者 for 循环来包含 Hook 的原因

如果想有条件的执行一个 Hook,可以将判断放到 Hook 内部

useEffect(function persistForm() {
  if (name !== '') localStorage.setItem('formData', name)
})

自定义 Hook

如果一个 Hook 需要应用在多个函数中,可以将 Hook 提取出来

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

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

function useFriendStatus(friendID) {
  // 添加一个状态isOnline,初始值为null
  const [isOnline, setIsOnline] = useState(null)
  // 每次渲染都会执行useEffect
  useEffect(() => {
    // 将isOnline更新为status.isOnline
    function handleStatusChange(status) {
      setIsOnline(status.isOnline)
    }

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

  return isOnline
}

useFriendStatus 就是一个自定义 Hook,自定义 Hook 不需要具有特殊的标识,可以自由决定参数是什么,以及它应该返回什么,除了名字要用 use 开头之外,它就像一个普通函数一样

useFriendStatus 的作用是订阅好友在线状态,所以需要将 ID 作为参数

function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null)
  //...
  return isOnline
}

然后就可以使用它了:

// 好友在线状态
function FriendStatus(props) {
  const isOnline = useFriendStatus(props.friend.id)

  if (isOnline === null) return 'Loading...'

  return isOnline ? 'Online' : 'Offline'
}
// 联系人列表在线状态
function FriendListItem(props) {
  const isOnline = useFriendStatus(props.friend.id)

  return (
    <li style={{ color: isOnline ? 'green' : 'black' }}>{props.friend.name}</li>
  )
}

在两个组件中使用相同的 Hook 不会共享同一个 state,因为自定义 Hook 是一个重用状态逻辑的机制,所以每次调用 Hook 的时候,其中所有 state 和副作用都是完全隔离的

多个 Hook 间传递信息

// 联系人列表
const friendList = [
  { id: 1, name: 'Phoebe' },
  { id: 2, name: 'Rachel' },
  { id: 3, name: 'Ross' }
]

function ChatRecipientPicker() {
  // 存储出初始ID
  const [recipientID, setRecipientID] = useState(1)
  // 将ID传入,判断好友在线状态
  const isRecipientOnline = useFriendStatus(recipientID)

  return (
    <div>
      {/*显示好友是否在线*/}
      <Circle color={isRecipientOnline ? 'green' : 'red'} />
      {/*在选择改变的时候获取当前选择联系人ID*/}
      <select
        value={recipientID}
        onChange={e => setRecipientID(Number(e.target.value))}
      >
        {/*遍历联系人列表*/}
        {friendList.map(friend => (
          <option key={friend.id} value={friend.id}>
            {friend.name}
          </option>
        ))}
      </select>
    </div>
  )
}

这里我们用到了两个 Hook:

  • useState
  • useFriendStatus

由于 useState 为我们提供了 recipientID 状态变量的最新值,因此我们可以将它作为参数传递给自定义的 useFriendStatus Hook