阅读 3264
React系列--Hook真的很简单

React系列--Hook真的很简单

「这是我参与11月更文挑战的第1天,活动详情查看:2021最后一次更文挑战

一直想学(懒得学)React Hook,今天认真看完了官方文档,不得不说Hook真香,为维护复杂数据状态的类组件提供了解决方案。

Hook,顾名思义就是钩子函数,也就是说本质上Hook是函数,具体点就是Hook是React 16.8版本内部新增的应用于函数组件对外的函数。

一般情况下,useState和useEffect这两个Hook以及自定义Hook可以满足大多数应用场景。

一般useState称作State Hook,useEffect称作Effect Hook。

Hook 简介

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

Hook没破坏性改动:

  • 完全可选的
  • 100%向后兼容
  • 现在可用

React官方没有计划移除class,无需重构现有类组件代码,如果你目前在编写类组件的React应用,后续可以尝试在项目使用Hook编写,渐进使用它们,Class和Hook互不影响。

Hook 解决了什么问题?

  • 无需修改组件结构的情况下复用状态逻辑。
  • 将组件中相互关联的部分拆分更小的函数,使其更容易理解
  • 摆脱了新手理解class以及this的困难

State Hook

State Hook就是useState钩子函数,接下来举例对比Class和Hook并分析useState到底做了什么事。

「Class示例」

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

  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示例」

import React, { useState } from 'react';

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

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}
复制代码

useState接收唯一的参数(数字、字符串...也可以是对象),并返回两个值存放在数组中,我这里用x,y表示,因此其返回值为[x, y],其中参数表示state初始值,x表示更新后的state,y表示更新state的函数,组件首次挂载时,state等于useState的传参。

Hook示例中代码 const [count, setCount] = useState(0) 表示我们声明了一个叫count的state变量和一个更新state变量的方法(功能等同于this.setState()),使用ES6数组解构的方式被桉顺序赋值,其中count被赋值x,setCount被赋值y。

在函数作用域中,可以直接使用变量,不带this,因此在Hook示例中,直接使用state变量和setCount()方法。

Effect Hook

Effect Hook就是useEffect钩子函数。

useEffect等同于Raact的生命周期componentDidMount()、componentDidUpdate()和compnentWillUnmount()三个函数的组合。

可以在useEffect中进行数据请求、设置订阅、操作DOM等操作。

接下来举例对比Class和Hook并分析useEffect。

分两种,无需清除的Effect和需要清除的Effect.

无需清除的Effect

「Class示例」

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

  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>
    );
  }
}
复制代码

「等价的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>
  );
}
复制代码

此处的的useEffect等同于componentDidMount()、componentDidUpdate()的组合。

需要清除的Effect

举个获取好友状态的例子对比展示

「Class示例」

class FriendStatus extends React.Component {
  constructor(props) {
    super(props);
    this.state = { isOnlinenull };
    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';
  }
}
复制代码

「等价的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);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

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

需要清除的Effect,指的是useEffect会返回一个函数,代替componentWillUnmount()函数执行解除订阅。 此处的的useEffect等同于componentDidMount()、componentDidUpdate()和componentWillUnmount()的组合。

Hook规则

使用它时需要遵循两条规则:

  • 只在最顶层使用Hook

  • 只在React函数中调用Hook

    • 在React函数组件中调用Hook
    • 在自定义Hook中调用其他Hook

对于第一条官方给的规则,我理解的是只在最顶层函数作用域使用Hook,官方说不要在循环,条件或嵌套函数中调用 Hook,主要是确保Hook每次渲染都按照同样的顺序被调用,这让React能够在多次的useState和useEffect调用之间保持Hook状态的正确。

举个例子:

正常遵守规则情况下,多个Hook的执行顺序:

function Form({
  // 1. Use the name state variable
  const [name, setName] = useState('Mary');

  // 2. Use an effect for persisting the form
  useEffect(function persistForm({
    localStorage.setItem('formData', name);
  });

  // 3. Use the surname state variable
  const [surname, setSurname] = useState('Poppins');

  // 4. Use an effect for updating the title
  useEffect(function updateTitle({
    document.title = name + ' ' + surname;
  });

  // ...
  
  
  // ------------
  // 首次渲染
  // ------------
  useState('Mary')           // 1. 使用 'Mary' 初始化变量名为 name 的 state
  useEffect(persistForm)     // 2. 添加 effect 以保存 form 操作
  useState('Poppins')        // 3. 使用 'Poppins' 初始化变量名为 surname 的 state
  useEffect(updateTitle)     // 4. 添加 effect 以更新标题

  // -------------
  // 二次渲染
  // -------------
  useState('Mary')           // 1. 读取变量名为 name 的 state(参数被忽略)
  useEffect(persistForm)     // 2. 替换保存 form 的 effect
  useState('Poppins')        // 3. 读取变量名为 surname 的 state(参数被忽略)
  useEffect(updateTitle)     // 4. 替换更新标题的 effect

  // ...
  }
复制代码

只要 Hook 的调用顺序在多次渲染之间保持一致,React 就能正确地将内部 state 和对应的 Hook 进行关联。 但如果我们将一个 Hook (例如 persistForm effect) 调用放到一个条件语句中会发生什么呢?

 // 在条件语句中使用 Hook 违反第一条规则
  if (name !== '') {
    useEffect(function persistForm({
      localStorage.setItem('formData', name);
    });
  }
复制代码

在第一次渲染中 name !== '' 这个条件值为 true,所以我们会执行这个 Hook。但是下一次渲染时我们可能清空了表单,表达式值变为 false。此时的渲染会跳过该 Hook,Hook 的调用顺序发生了改变:

useState('Mary')           // 1. 读取变量名为 name 的 state(参数被忽略)
// useEffect(persistForm)  // 此 Hook 被忽略!
useState('Poppins')        // 2 (之前为 3)。读取变量名为 surname 的 state 失败
useEffect(updateTitle)     // 3 (之前为 4)。替换更新标题的 effect 失败
复制代码

React 不知道第二个 useState 的 Hook 应该返回什么。React 会以为在该组件中第二个 Hook 的调用像上次的渲染一样,对应的是 persistForm 的 effect,但并非如此。从这里开始,后面的 Hook 调用都被提前执行,导致 bug 的产生。

**「是为什么 Hook 需要在我们组件的最顶层调用」**果我们想要有条件地执行一个 effect,可以将判断放到 Hook 的内部:

useEffect(function persistForm({
    // 👍 将条件判断放置在 effect 中
    if (name !== '') {
      localStorage.setItem('formData', name);
    }
  });
复制代码

注:官方为我们提供了lint插件eslint-plugin-react-hooks,使用了就无需担心此问题!

自定义Hook

自定义Hook更像是一种约定,必须以use开头来命名函数。

自定义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);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

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

组件二:聊天应用中有一个联系人列表,当用户在线时需要把名字设置为绿色

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

function FriendListItem(props{
  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);
    };
  });

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

提取自定义Hook,返回好友在线状态Boolean值

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:

function FriendStatus(props{
  const isOnline = useFriendStatus(props.friend.id);

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

组件二使用自定义Hook

function FriendListItem(props{
  const isOnline = useFriendStatus(props.friend.id);

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

代码逻辑清晰度显而易见。

Hook API 索引

一般情况下useState和useEffect就可以满足我们的需求,此处贴下所有Hook,有业务场景可以传送到官网学习下。

总结

以上例子和部分术语参考React官方文档,适合React新手学习参考,本人也是React新手,后期会计划边学边输出React基础系列,与大家共同成长。

文章分类
前端
文章标签