- 原文地址:Best Practices With React Hooks
- 原文作者:Adeneye David Abiodun
- 译文出自:掘金翻译计划
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。
请参阅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 函数,但在使用时需要遵循两个规则。
注意:这两条规则是在 React Hooks 中引入的,而不是作为 JavaScript 本身的一部分。
让我们更详细地看看这些规则。
在顶层调用Hooks
不要在循环、条件或嵌套函数中调用 Hooks 。应该始终在 React 函数的顶层使用 Hooks 。通过遵循此规则,可以确保每次渲染组件时以相同的顺序调用 Hooks。这就是允许 React 在多次调用 useState 和useEffect 之间正确保留 Hooks 状态的原因。
让我们设计一个具有两种状态的Form组件:
accountNameaccountDetail
这些状态具有默认值,我们将使用useEffect钩子将状态持久化到浏览器的本地存储或文档的标题中。
现在,如果在多次调用useState和useEffect之间保持相同状态,那么该组件可能会成功管理其状态。
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在调用useState和useEffect 的自定义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的功能。在这样的地方,除非需要,我们不必再调用useState或useEffect。
通过遵循此规则,可以确保组件中的所有有状态逻辑在其源代码中都清晰可见。
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元素的对象。然后我们可以在另一个组件中导入和渲染这个组件。
该类式组件使用称为封装的编程方法,这基本上意味着与类式组件相关的所有内容都将存在于其中。生命周期方法(constructors、componentDidMount()、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 中使用函数式组件有一些好处:
- 分离容器和展示组件会变得更容易,因为如果无法访问组件中的
setState(),则需要更多地考虑组件的状态。 - 函数式组件更易于阅读和测试,因为它们是没有状态或生命周期Hooks的纯 JavaScript 函数。
- 你最终会得到更少的代码。
- 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 片段添加到项目中:
- 命令
按下VS Code的快捷键(Ctrl+P), 输入
ext install ALDuncanson.react-hooks-snippets然后按回车健. - 扩展市场 打开‘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 项目中吸取这里学到的所有经验教训。祝你好运!
资源
- “Introducing Hooks,” React Docs
- “Functional vs Class-Components In React,” David Jöch, Medium
- “Mixins Considered Harmful,” Dan Abramov, React Blog
- “React Hooks: Best Practices And A Shift In Mindset,” Bryan Manuele, Medium
- “React Hooks Snippets For VS Code,” Anthony Davis, Visual Code Marketplace