【译】Best Practices With React Hooks

394 阅读11分钟

React Hooks 的最佳实践

摘要 ↬ 本文涵盖了 React Hooks 的规则以及如何在项目中有效地开始使用它们。请注意,为了详细阅读本文,你首先需要知道如何使用 React Hooks

React Hooks 是 React 16.8 中的新增功能,可以在不编写类(class)组件的情况下使用状态(state)和其他 React 功能。换句话说,Hooks是让你从函数组件 “挂钩(hook into)” 到 React 状态(state)和生命周期特性的函数。(它们在类组件内部不起作用。)

React 提供了一些内置的 Hooks,比如useState. 还可以创建自己的 Hooks 以在不同组件之间重用有状态行为。这个例子展现了一个计数器,它的状态是使用useState() hook 管理的。每次单击按钮时,我们都会使用setCount()来更新count的值使其增加1。

GIF 2021-11-4 11-45-12.gif

请参阅Adeneye Abiodun David 的 Pen React Hook example with Counter

此示例呈现一个值为 0 的计数器 。单击该按钮时,它的值会增加1。组件的初始值是使用useState定义的。

const [count, setCount] = useState(0)

如您所见,我们将其设置为 0。然后我们在想要增加值时使用 onClick() 方法调用 setCount

<button onClick={() => setCount(count + 1)}>
  Click me
</button>

在 React Hooks 发布之前,我们不得不使用一个组件,所以会导致这个例子使用更多的代码。

React Hooks 的规则

在深入研究最佳实践之前,我们需要了解 React Hooks 的规则,这也是本文中介绍的实践的一些基本概念。

React Hooks 是 JavaScript 函数,但在使用时需要遵循两个规则。

  1. 在顶层调用Hooks
  2. 只在React组件中调用Hooks

注意:这两条规则是在 React Hooks 中引入的,而不是作为 JavaScript 本身的一部分。

让我们更详细地看看这些规则。

在顶层调用Hooks

不要在循环、条件或嵌套函数中调用 Hooks 。应该始终在 React 函数的顶层使用 Hooks 。通过遵循此规则,可以确保每次渲染组件时以相同的顺序调用 Hooks。这就是允许 React 在多次调用 useStateuseEffect 之间正确保留 Hooks 状态的原因。

让我们设计一个具有两种状态的Form组件:

  • accountName
  • accountDetail

这些状态具有默认值,我们将使用useEffect钩子将状态持久化到浏览器的本地存储或文档的标题中。

现在,如果在多次调用useStateuseEffect之间保持相同状态,那么该组件可能会成功管理其状态。

function Form() {
  // 1. 使用accountName状态变量
  const [accountName, setAccountName] = useState('David');

  // 2. 使用effect来持久保存表单
  useEffect(function persistForm() {
    localStorage.setItem('formData', accountName);
  });

  // 3. 使用accountDetail状态变量
  const [accountDetail, setAccountDetail] = useState('Active');

  // 4. 使用一个effect来更新标题
  useEffect(function updateStatus() {
    document.title = accountName + ' ' + accountDetail;
  });

  // ...
}

如果 Hooks 的顺序发生变化(当它们在循环或条件中被调用时是可能的),React 将很难弄清楚如何保持组件的状态。

// ------------
useState('David')           // 1. 用'David'初始化accountName状态变量
useEffect(persistForm)     // 2. 添加用于持久保存表单的effect
useState('Active')        // 3. 用'Active'初始化accountdetail状态变量
useEffect(updateStatus)     // 4. 添加更新状态的effect

// -------------
// Second render
// -------------
useState('David')           // 1. 读取accountName状态变量(忽略参数)
useEffect(persistForm)     // 2.替换持久保存表单的effect
useState('Active')        // 3. 读取accountDetail状态变量(忽略参数)
useEffect(updateStatus)     // 4. 替换更新状态的effect

// ...

这就是 React 调用我们的Hook的顺序。由于顺序保持不变,它将能够保留我们组件的状态。但是如果我们将 Hook 调用放入条件中会发生什么?

// 🔴 在条件中使用Hook违背了第一条规则
  if (accountName !== '') {
    useEffect(function persistForm() {
      localStorage.setItem('formData', accountName);
    });
  }

在第一次渲染时accountName !== ''条件是true,所以我们运行此Hook。但是,在下一次渲染时,用户可能会清除表单,使条件变为false。现在我们在渲染过程中跳过了这个 Hook,因此 Hook 调用的顺序变得不同:

useState('David')           // 1. 读取accountName状态变量(忽略参数)
// useEffect(persistForm)  // 🔴 这个Hook被跳过了!
useState('Active')        // 🔴 2(但为3). 读取状态变量accountDetails失败
useEffect(updateStatus)     // 🔴 3(但是4). 未能替代effect

React 不知道第二次 useState 调用 Hook 会返回什么。React 期望这个组件中的第二个 Hook 调用对应于 persistForm effect,就像在前一个渲染期间一样——但它不再是了。从那时起,Hook我们跳过的每个调用之后的下一个调用也会移动一个 - 导致错误。

这就是必须在组件顶层调用 Hooks 的原因。如果我们想在有条件的情况下运行一个effect,我们可以将该条件放在我们的 Hook 中。

注意:可以查看React Hook 文档以阅读有关于此主题的更多信息。

仅从 React 组件调用Hooks

不要在常规 JavaScript 函数中调用 Hooks。相反,可以从 React 函数组件调用 Hook。下面我们来看看 JavaScript 函数和 React 组件的区别:

JavaScript 函数

import { useState } = "react";

function toCelsius(fahrenheit) {
  const [name, setName] = useState("David");
  return (5/9) * (fahrenheit-32);
}
document.getElementById("demo").innerHTML = toCelsius;

这里我们 从 React 包中导入 useState Hook,然后声明函数。但这是无效的,因为这不是 React 组件。

React 函数

import React, { useState} from "react";
import ReactDOM from "react-dom";

function Account(props) {
  const [name, setName] = useState("David");
  return <p>Hello, {name}! The price is <b>{props.total}</b> and the total amount is <b>{props.amount}</b></p>
}
ReactDom.render(
  <Account total={20} amount={5000} />,
  document.getElementById('root')
);

尽管两者的主体看起来相似,但当我们将 React 导入时,后者变成了一个组件。这使得我们可以在内部使用JSX和 React Hooks之类的东西。

如果碰巧导入了你喜欢的钩子而不导入 React(这使其成为常规函数),你将无法使用你导入的 hook,因为该 hook 只能在 React 组件中访问。

从自定义hooks中调用hooks

自定义 Hooks 是一个 JavaScript 函数,其名称以 use 开头,并且可以调用其他 Hooks。例如,useUserName在调用useStateuseEffect 的自定义Hook下面使用。它从 API 获取数据,循环访问数据,并在 API 数据中存在它收到的特定用户名时调用setIsPresent()

export default function useUserName(userName) {
  const [isPresent, setIsPresent] = useState(false);
  
  useEffect(() => {
    const data = MockedApi.fetchData();
    data.then((res) => {
      res.forEach((e) => {
        if (e.name === userName) {
          setIsPresent(true);
        }
     });
    });
  });

  return isPresent;
}

然后我们可以继续在我们的应用程序中需要的其他地方重用这个Hook的功能。在这样的地方,除非需要,我们不必再调用useStateuseEffect

通过遵循此规则,可以确保组件中的所有有状态逻辑在其源代码中都清晰可见。

ESLINT 插件

调用 ESLint 插件eslint-plugin-react-hooks强制执行上述规则。这在项目执行时非常方便。建议在处理项目时使用此插件,尤其是在与他人合作时。如果想尝试,可以将此插件添加到项目中:

// Your ESLint configuration
{
  "plugins": [
    // ...
    "react-hooks"
  ],
  "rules": {
    // ...
    "react-hooks/rules-of-hooks": "error", // Checks rules of Hooks
    "react-hooks/exhaustive-deps": "warn" // Checks effect dependencies
  }
}

这个插件默认包含在Create React App 中。因此,如果使用 Create-React-App 引入 React 应用程序,则不需要添加它。

Hooks中的思考

深入研究几个 Hook 最佳实践之前,让我们简要介绍一下类式(class)组件和函数式(functional)组件(和 Hooks)。

在 React 中定义组件的最简单方法是编写一个返回 React 元素(React element)的 JavaScript 函数:

function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}

Welcome组件接受 props参数, 其是一个包含数据的对象,并返回一个React元素的对象。然后我们可以在另一个组件中导入和渲染这个组件。

该类式组件使用称为封装的编程方法,这基本上意味着与类式组件相关的所有内容都将存在于其中。生命周期方法(constructorscomponentDidMount()render等)为组件提供了可预测的结构。

封装是OOP(面向对象编程Object-Oriented Programming)的基本原则之一。它指的是在对数据进行操作的方法中绑定数据,并用于隐藏类中结构化数据对象的值或状态——防止未经授权的地方直接访问它们。

使用 Hooks,组件的组成从生命周期 Hooks 的组合转变为最终带有一些渲染的功能。

函数式组件

下面的示例展示了如何在函数组件中使用自定义 Hooks(不展示主体是什么)。然而,它做什么或可以做什么是不受限制的。它可以是实例化状态变量、使用上下文、为组件订阅各种副作用(side effects)——或者如果使用自定义钩子,则可以执行以上所有操作!

function {
  useHook{...};
  useHook{...};
  useHook{...};
  return (
    ...
  );
}

类式组件

类式组件要求从继承自React.Component,并创建一个返回一个React元素的render函数。这需要更多代码,但也会带来一些好处。

class {
  constructor(props) {...}
  componentDidMount() {...}
  componentWillUnmount() {...}
  render() {...}
}

在 React 中使用函数式组件有一些好处:

  1. 分离容器和展示组件会变得更容易,因为如果无法访问组件中的setState(),则需要更多地考虑组件的状态。
  2. 函数式组件更易于阅读和测试,因为它们是没有状态或生命周期Hooks的纯 JavaScript 函数。
  3. 你最终会得到更少的代码。
  4. React 团队提到,在未来的 React 版本中,函数式组件的性能可能会有所提升。

这是使用 React Hooks 的第一个最佳实践。

使用 Hooks 的最佳实践

1.简化 Hooks

保持 React Hooks 简单将能够有效地控制和操纵组件在其整个生命周期中发生的事情。尽可能避免编写自定义 Hooks;可以使用内置的 auseState()useEffect()而不是创建自己的Hooks。

如果发现自己使用了一堆在函数上相关的自定义 Hook,那可以创建一个自定义 Hook,作为这些的封装(wrapper)。下面我们来看看两个不同的带有Hooks的函数式组件。

函数组件 v1

function {
  useHook(...);
  useHook(...);
  useHook(...);
  return(
    <div>...</div>
  );
}

函数组件 v2

function {
  useCustomHook(...);
    useHook(...);
    useHook(...);
  return(
    <div>...</div>
  );
}

v2 是一个更好的版本,因为它使Hook保持简单,并且所有其他useHooks 都相应地在内部相关联。这使我们能够创建可在不同组件之间重用的函数,并赋予我们更多的能力来有效地控制和操纵我们的组件。应该使用 v2,而不是采用在其中组件散布着 Hooks的v1,这将使调试更容易并使代码更清晰。

2. 组织和构造你的Hooks

React Hooks 的优势之一是能够编写更少的易于阅读的代码。在某些情况下,大量的useEffect()useState()仍然可能令人困惑。当保持组件井井有条时,它将有助于提高可读性并保持组件流的一致性和可预测性。如果自定义 Hook 太复杂,你可以随时将它们分解为子自定义 Hook。将组件的逻辑提取到自定义 Hooks 中,使代码可以被阅读明白。

3. 使用 REACT HOOKS 片段

React Hooks Snippets 是一个 Visual Studio Code 扩展,使得创建 React Hooks 变得更简单、更快速。目前支持五个Hooks:

  • useState()
  • useEffect()
  • useContext()
  • useCallback()
  • useMemo()

还添加了其他片段。我尝试过使用这些 Hooks,这是我在使用它们时个人使用的最佳实践之一。

有两种方法可以将 React Hooks 片段添加到项目中:

  1. 命令 按下VS Code的快捷键(Ctrl+P), 输入 ext install ALDuncanson.react-hooks-snippets 然后按回车健.
  2. 扩展市场 打开‘VS Code 扩展市场’ (Ctrl+Shift+X) 然后搜索‘React Hook Snippets’. 接着就可以看到 ‘Alduncanson’ 图标.

推荐第一个代码段。了解更多有关代码段在这里或检查的最新的Hooks代码段在这里

4. 考虑 HOOKS 规则

努力在使用 React Hooks 时始终考虑我们之前学到的 Hooks 的两个规则。

  • 只在顶层调用你的 Hooks。不要在循环、条件或嵌套函数中调用 Hook。
  • 始终从 React 函数组件或自定义 Hooks 中调用 Hooks,不要从常规 JavaScript 函数调用 Hooks。

使用 ESlint 插件eslint-plugin-react-hooks强制执行这两个规则,如果愿意,可以将此插件添加到项目中,正如我们在上面的Hooks规则部分所解释的那样。

最佳实践尚未完全解决,因为 Hooks 仍然相对较新。因此,在采用任何早期技术时都应采取预防措施。考虑到这一点,Hooks 是 React 未来的方向。

总结

我希望你喜欢这个教程。我们已经学习了 React Hooks 的两个最重要的规则以及如何在 Hooks 中有效地思考。我们研究了函数式组件和一些以正确有效的方式编写 Hook 的最佳实践。尽管规则很简短,但在编写规则时让它们成为你的指南针很重要。如果容易忘记它,可以使用 ESLint 插件来强制执行它。

我希望你能在你的下一个 React 项目中吸取这里学到的所有经验教训。祝你好运!

资源