美文网首页
React Hook

React Hook

作者: 灭绝小师弟 | 来源:发表于2020-04-10 21:20 被阅读0次

    什么是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 (
        <div>
          <p>You clicked {count} times</p>
          <button onClick={() => setCount(count + 1)}>
            Click me
          </button>
        </div>
      );
    }
    

    class组件

    class Example extends React.Component {
      constructor(props) {
        super(props);
        this.state = {
          count: 0
        };
      }
    
      render() {
        return (
          <div>
            <p>You clicked {this.state.count} times</p>
            <button onClick={() => this.setState({ count: this.state.count + 1 })}>
              Click me
            </button>
          </div>
        );
      }
    }
    

    可以看出: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 (
        <div>
          <p>You clicked {count} times</p>
          <button onClick={() => setCount(count + 1)}>
            Click me
          </button>
        </div>
      );
    }
    

    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 (
          <div>
            <p>You clicked {this.state.count} times</p>
            <button onClick={() => this.setState({ count: this.state.count + 1 })}>
              Click me
            </button>
          </div>
        );
      }
    }
    

    需要清除的 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 (
        <li style={{ color: isOnline ? 'green' : 'black' }}>
          {props.friend.name}
        </li>
      );
    }
    

    Hook API 索引

    Hook FAQ

    Hook的常见问题

    相关文章

      网友评论

          本文标题:React Hook

          本文链接:https://www.haomeiwen.com/subject/sysophtx.html