React Hook

什么是Hook

Hook 是 React 16.8 的新增特性,用途是在函数组件中使用state、生命周期函数等其他react特性,而不用class组件。

Hook 使用规则

Hook 就是 JavaScript 函数。

  • 只能在函数最外层调用 Hook。不要在循环、条件判断或者子函数中调用。
  • 只能在 React 的函数组件和自定义的 Hook 中调用 Hook。不要在其他 JavaScript 函数中调用。

State Hook

在编写函数组件时,如果需要添加state,则可以使用state hook,而不用像以前一样需要改写成class组件。

Hook 在 class 内部是不起作用的。

useState(),接收一个参数state的初始值(数值,字符串,对象皆可),返回一个数组。数组的第一个值是当前的state,第二个值是更新state的函数。使用数组解构的方式来接收这两个值。一般格式为以下:

// 设置something初始值为'defaultValue'
const [something, setSomething] = useState('defaultValue') 

通过下面例子,可以比较使用hook和class组件不同:
Hook

import React, { useState } from 'react';

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

  return (
    

You clicked {count} times

); }

class组件

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

  render() {
    return (
      

You clicked {this.state.count} times

); } }

可以看出:count等价于this.state.count,setCount(count+1)等价于this.setState({count: count+1})。

使用多个 state 变量

使用多个state,只需要多次调用useState()。也可以用一个对象来储存所有的state,但是这样更新state时,需要自己处理其中的合并逻辑(通常使用扩展运算符...),具体可参考FAQ中关于分离独立 state 变量的建议。

Effect Hook

Effect Hook 使我们可以在函数组件中执行副作用操作。

  • 数据获取,设置订阅以及手动更改 React 组件中的 DOM 都属于副作用。
  • useEffect() 可以看做 componentDidMount,componentDidUpdate 和 componentWillUnmount 的组合。

无需清除的 effect

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

useEffect()

useEffect()接收两个参数,第一个参数是一个函数,并且在执行 DOM 更新之后调用这个函数,第二个是可选参数,是一个数组,如果数组中的值在两次重渲染之间没有发生变化,可以通知 React 跳过对 effect 的调用。这个函数通常称之为effect,即副作用。使用这个hook,可以告诉 React 组件需要在渲染后执行某些操作。

  • 将 useEffect 放在组件内部,使我们可以在 effect 中直接访问 state 变量(或其他 props)。
  • 默认情况下,第一次渲染之后和每次更新之后都会执行useEffect。
  • 保证了每次运行 effect 的同时,DOM 都已经更新完毕,不用再去考虑“挂载”还是“更新”。
  • 使用 useEffect 调度的 effect 不会阻塞浏览器更新屏幕,这样使得应用看起来响应更快。

通过下面例子,将 document 的 title 设置为包含了点击次数的消息,可以比较使用hook和class组件不同:
hook

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

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

  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

  return (
    

You clicked {count} times

); }

class

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 (
      

You clicked {this.state.count} times

); } }

需要清除的 effect

有一些副作用是需要清除的,例如订阅外部数据源。这种情况下,清除工作是非常重要的,可以防止引起内存泄露。
class组件通常会在 componentDidMount 中设置订阅,并在 componentWillUnmount 中清除它。
通过下面例子,假设我们有一个 ChatAPI 模块,它允许我们订阅好友的在线状态,可以比较使用hook和class组件不同:

  • 如果 effect 返回一个函数,React 将会在执行清除操作时调用它。这是 effect 可选的清除机制。每个 effect 都可以返回一个清除函数。如此可以将添加和移除订阅的逻辑放在一起。它们都属于 effect 的一部分。
  • React 会在组件卸载的时候执行清除操作。
  • 并不是必须为 effect 中返回的函数命名,也可以返回一个箭头函数。

hook

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

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

  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    // Specify how to clean up after this effect:
    return function cleanup() {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

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

class

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

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

用 Hook 其中一个目的就是要解决 class组件 中生命周期函数经常包含不相关的逻辑,但又把相关逻辑分离到了几个不同方法中的问题。就像使用多个 state 的 Hook一样,也可以使用多个 effect,将不相关逻辑分离到不同的 effect 中。

  • Hook 允许按照代码的用途分离各个effect,而不是像生命周期函数那样。
  • React 将按照 effect 声明的顺序依次调用组件中的每一个 effect。
    通过下面例子可以发现,在class组件中,设置 document.title 的逻辑被分割到 componentDidMount 和 componentDidUpdate 中,订阅逻辑被分割到 componentDidMount 和 componentWillUnmount 中。而且 componentDidMount 中同时包含了两个不同功能的代码。
    hook
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

class FriendStatusWithCounter 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() {
    document.title = `You clicked ${this.state.count} times`;
  }

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

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

为什么每次更新的时候都要运行 Effect

effect 的清除阶段在每次重新渲染时都会执行,而不是只在卸载组件的时候执行一次。这个设计可以帮助我们创建 bug 更少的组件。
示例,从 class 中 props 读取 friend.id,然后在组件挂载后订阅好友的状态,并在卸载组件的时候取消订阅:

componentDidMount() {
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

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

但是当组件已经显示在屏幕上时,friend prop 发生变化时,这个组件将继续展示原来的好友状态。这是一个 bug,而且还会因为取消订阅时使用错误的好友 ID 导致内存泄露或崩溃的问题。内存泄露是指当一块内存不再被应用程序使用的时候,由于某种原因,这块内存没有返还给操作系统或者内存池的现象。
在 class 组件中,需要添加 componentDidUpdate 来解决这个问题:

componentDidMount() {
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  componentDidUpdate(prevProps) {
    // 取消订阅之前的 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
    );
  }

hook

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

并不需要特定的代码来处理更新逻辑,因为 useEffect 默认就会处理。它会在调用一个新的 effect 之前对前一个 effect 进行清理。例如

// 挂载: { friend: { id: 100 } }
ChatAPI.subscribeToFriendStatus(100, handleStatusChange);     // 运行第一个 effect

// 更新: { friend: { id: 200 } }
ChatAPI.unsubscribeFromFriendStatus(100, handleStatusChange); // 清除上一个 effect
ChatAPI.subscribeToFriendStatus(200, handleStatusChange);     // 运行下一个 effect

// 更新: { friend: { id: 300 } }
ChatAPI.unsubscribeFromFriendStatus(200, handleStatusChange); // 清除上一个 effect
ChatAPI.subscribeToFriendStatus(300, handleStatusChange);     // 运行下一个 effect

// 卸载
ChatAPI.unsubscribeFromFriendStatus(300, handleStatusChange); // 清除最后一个 effect

此默认行为保证了一致性,避免了在 class 组件中因为没有处理更新逻辑而导致常见的 bug。

通过跳过 Effect 进行性能优化

在某些情况下,每次渲染后都执行清理或者执行 effect 可能会导致性能问题。
在 class 组件中,可以通过在 componentDidUpdate 中添加对 prevProps 或 prevState 的比较逻辑解决:

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

在 useEffect 中,如果某些特定值在两次重渲染之间没有发生变化,可以传递数组作为 useEffect 的第二个可选参数,通知 React 跳过对 effect 的调用。

useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]); // 仅在 count 更改时更新

对于有清除操作的 effect 同样适用:

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

  ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
  return () => {
    ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
  };
}, [props.friend.id]); // 仅在 props.friend.id 发生变化时,重新订阅
  • 如果要使用此优化方式,需确保数组中包含了所有外部作用域中会随时间变化并且在 effect 中使用的变量,否则会引用到先前渲染中的旧变量。
  • 如果想执行只运行一次的 effect(仅在组件挂载和卸载时执行),可以传递一个空数组([])作为第二个参数。这就告诉 React 你的 effect 不依赖于 props 或 state 中的任何值,所以它永远都不需要重复执行。

Hook规则

只在最顶层使用 Hook
不要在循环,条件或嵌套函数中调用 Hook, 确保总是在 React 函数的最顶层调用。遵守这条规则,能确保 Hook 在每一次渲染中都按照同样的顺序被调用。这让 React 能够在多次的 useState 和 useEffect 调用之间保持 hook 状态的正确。如果想要有条件地执行一个 effect,可以将判断放到 Hook 的内部。
只在 React 函数中调用 Hook
不要在普通的 JavaScript 函数中调用 Hook。

  • 在 React 的函数组件中调用 Hook。
  • 在自定义 Hook 中调用其他 Hook。
    遵循此规则,确保组件的状态逻辑在代码中清晰可见。
    可以通过 eslint-plugin-react-hooks 的 ESLint 插件来强制执行这两条规则。
npm install eslint-plugin-react-hooks --save-dev // 安装
// eslint配置
{
  "plugins": [
    // ...
    "react-hooks"
  ],
  "rules": {
    // ...
    "react-hooks/rules-of-hooks": "error", // 检查 Hook 的规则
    "react-hooks/exhaustive-deps": "warn" // 检查 effect 的依赖
  }
}

自定义 Hook

自定义 Hook 是一个函数,其名称以 “use” 开头,函数内部可以调用其他的 Hook。
两个函数之间有共享逻辑时,通常会把共用部分提取到第三个函数中。而组件和 Hook 都是函数,所以也同样适用这种方式。

提取自定义Hook

useFriendStatus 自定义Hook

import { useState, useEffect } from 'react';

function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null);

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

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

  return isOnline;
}
  • 确保只在自定义 Hook 的顶层无条件地调用其他 Hook,与组件中使用Hook的规则一致。
  • 函数名字必须以 use 开头,参数与返回无要求,根据实际情况自定义

使用自定义Hook

在 FriendStatus 和 FriendListItem 组件中使用自定义的useFriendStatus Hook。

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 (
    
  • {props.friend.name}
  • ); }

    Hook API 索引

    • 基础 Hook

      • useState
      • useEffect
      • useContext
    • 额外的 Hook

      • useReducer
      • useCallback
      • useMemo
      • useRef
      • useImperativeHandle
      • useLayoutEffect
      • useDebugValue

    Hook FAQ

    Hook的常见问题

    你可能感兴趣的:(React Hook)