tao-of-react

最佳实践的一些原则和建议

  1. Components
    1. Favor Functional Components
    2. Write Consistent Components
    3. Name Components
    4. Organize Helper Functions
    5. Don’t Hardcode Markup
    6. Component Length
    7. Component Length
    8. Write Comments
    9. Use Error Boundaries
    10. Destructure Props
    11. Number of Props
    12. Use Objects Instead of Primitives
    13. Conditional Rendering
    14. Avoid Nested Ternary Operators
    15. Move Lists in Components
    16. Assign Default Props When Destructuring
    17. Avoid Nested Render Functions
    18. Prefer Hooks
    19. State Management
  2. Use Reducers
    1. Use Data Fetching Libraries
    2. State Management Libraries
    3. Component Mental models
  3. Container and Presentational
    1. Stateless and Stateful
    2. Application Structure
  4. Group by Route/Module
    1. Create a Common Module
    2. Use Absolute Paths
    3. Wrap External Components
    4. Move Components in Folders
  5. Performance
    1. Don’t Optimize Prematurely
    2. Watch Bundle Size
    3. Rerenders - Callbacks, Arrays and Objects
    4. Testing
  6. Don’t Rely on Snapshot Tests
    1. Test Correct Rendering
    2. Validate State and Events
    3. Test Edge Cases
    4. Write Integration Tests
    5. Styling
  7. Use CSS-in-JS
    1. Keep Styled Components Together
    2. Data Fetching
  8. Use a Data Fetching Library

Components

优先选择函数式组件

优先选择函数式组件,它有更简单的语法,没有生命周期方法,构造函数或者 boilerplate(模板)。你可以用更少的字符来表达相同的逻辑,而不会失去可读性。

除非你需要一个错误边界,否则函数式组件应该是你的首选方法。你需要记住的心智模式要少得多。

// 👎 Class components are verbose
class Counter extends React.Component {
state = {
counter: 0,
};

constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
}

handleClick() {
this.setState({ counter: this.state.counter + 1 });
}

render() {
return (
<div>
<p>counter: {this.state.counter}</p>
<button onClick={this.handleClick}>Increment</button>
</div>
);
}
}

// 👍 Functional components are easier to read and maintain
function Counter() {
const [counter, setCounter] = useState(0);

handleClick = () => setCounter(counter + 1);

return (
<div>
<p>counter: {counter}</p>
<button onClick={handleClick}>Increment</button>
</div>
);
}

编写一致的组件

为组件编写固定一种风格,将辅助(工具)函数放在同一个位置,使用一致的方式导出,并且遵循一致的命名原则

其中一种方法并不比另一种有真正的好处

无论你是在文件的底部导出还是直接在组件的定义中导出,选择一种方法并坚持使用它。

为组件命名,不要使用匿名组件

始终为组件命名,它可以帮助你读取错误堆栈跟踪并使用 React Dev Tools。

如果组件的名称在文件中,那么在开发时更容易找到文件所在的位置。

// 👎 Avoid this
export default () => <form>...</form>

// 👍 Name your functions
export default function Form() {
return <form>...</form>
}

组织你的辅助(工具)函数,与组件分离

不需要在组件上持有闭包的辅助(工具)函数应该被移到外部。理想的位置是在组件定义之前,这样文件就可以从上到下读取。

这也减少了组件的冗余程度,只留下那些需要在那里。

// 👎 Avoid nesting functions which don't need to hold a closure.
function Component({ date }) {
function parseDate(rawDate) {
...
}

return <div>Date is {parseDate(date)}</div>
}

// 👍 Place the helper functions before the component
function parseDate(date) {
...
}

function Component({ date }) {
return <div>Date is {parseDate(date)}</div>
}

您希望在定义中保留最少的辅助(工具)函数。将尽可能多的变量移到外部,并将 state 的值作为参数传递。用仅依赖输入的纯函数组成逻辑可以更容易地跟踪 bug 和扩展。

// 👎 Helper functions shouldn't read from the component's state
export default function Component() {
const [value, setValue] = useState('')

function isValid() {
// ...
}

return (
<>
<input
value={value}
onChange={e => setValue(e.target.value)}
onBlur={validateInput}
/>
<button
onClick={() => {
if (isValid) {
// ...
}
}}
>
Submit
</button>
</>
)
}

// 👍 Extract them and pass only the values they need
function isValid(value) {
// ...
}

export default function Component() {
const [value, setValue] = useState('')

return (
<>
<input
value={value}
onChange={e => setValue(e.target.value)}
onBlur={validateInput}
/>
<button
onClick={() => {
if (isValid(value)) {
// ...
}
}}
>
Submit
</button>
</>
)
}

不要编写硬编码

不要为导航、过滤器或列表硬编码标记。使用一个配置对象并循环遍历这些项。这样做在未来更改时只需要在单个位置更改标记和项。

// 👎 Hardcoded markup is harder to manage.
function Filters({ onFilterClick }) {
return (
<>
<p>Book Genres</p>
<ul>
<li>
<div onClick={() => onFilterClick("fiction")}>Fiction</div>
</li>
<li>
<div onClick={() => onFilterClick("classics")}>Classics</div>
</li>
<li>
<div onClick={() => onFilterClick("fantasy")}>Fantasy</div>
</li>
<li>
<div onClick={() => onFilterClick("romance")}>Romance</div>
</li>
</ul>
</>
);
}

// 👍 Use loops and configuration objects
const GENRES = [
{
identifier: "fiction",
name: Fiction,
},
{
identifier: "classics",
name: Classics,
},
{
identifier: "fantasy",
name: Fantasy,
},
{
identifier: "romance",
name: Romance,
},
];

function Filters({ onFilterClick }) {
return (
<>
<p>Book Genres</p>
<ul>
{GENRES.map((genre) => (
<li>
<div onClick={() => onFilterClick(genre.identifier)}>
{genre.name}
</div>
</li>
))}
</ul>
</>
);
}

组件长度

React 组件只是一个获取 props 并返回标记语言的函数。它们遵循相同的软件设计原则。

如果一个函数做了太多的事情,提取一些逻辑并包装为一个新的函数。

组件也是一样,如果你有太多的功能,把它分解成更小的组件,然后调用它们。

如果标记语言的一部分是复杂的,需要循环和条件渲染。那么就可以去抽象为一个新的组件

依靠 props 和回调进行通信和数据。代码行数不是一个客观的度量。考虑一下职责和抽象。

在 jsx 中编写注释

当某些东西需要更明确的时候,打开一个代码块并提供额外的信息。注释是逻辑的一部分,所以当您觉得某些东西需要更清晰时,就提供它。

function Component(props) {
return (
<>
{/* If the user is subscribed we don't want to show them any ads */}
{user.subscribed ? null : <SubscriptionPlans />}
</>
);
}

使用 <ErrorBoundary> 标签

一个组件中的错误不应该导致整个 UI 崩溃。在极少数情况下,如果发生了严重错误,我们希望删除整个页面或重定向。在大多数情况下,只要在屏幕上隐藏一个特定的元素就可以了。

在函数中你可能会有一些关于页面需要展示数据的处理,不要只在顶层设置错误边界。包装 在页面上可以单独存在的元素以避免级联失败。

function Component() {
return (
<Layout>
<ErrorBoundary>
<CardWidget />
</ErrorBoundary>

<ErrorBoundary>
<FiltersWidget />
</ErrorBoundary>

<div>
<ErrorBoundary>
<ProductList />
</ErrorBoundary>
</div>
</Layout>
);
}

解构 props

大多数 React 组件只是功能性的组件。他们得到 props,然后返还 渲染到页面上的标记语言。

在普通函数中,您使用直接传递的参数,因此在这里应用相同的原则是有意义的。不需要到处重复使用 props。

有一种不解构 props 的情况是为了区分什么是外部状态,什么是内部状态。

但在常规函数中,参数和变量之间没有区别。不要创造不必要的模式

// 👎 Don't repeat props everywhere in your component
function Input(props) {
return <input value={props.value} onChange={props.onChange} />;
}

// 👍 Destructure and use the values directly
function Component({ value, onChange }) {
const [state, setState] = useState("");

return <div>...</div>;
}

props 的数量

一个组件应该接收多少 props 是一个主观的问题。一个组件拥有的 props 数量与它做了多少事情相关。你给予它的支持越多,它的责任也就越大

大量的 props 是一个组件做太多的信号。

如果我超过 5 个 props,我考虑这个组件是否应该被拆分。

在某些情况下,它可能只需要大量数据。例如,一个输入字段可能有很多 props。但是在另一些情况下,它是需要被封装抽象的标志。

注意:使用的 props 数量越多,越可能被重复渲染

使用对象代替原始值

限制 props 数量的一种方法是传递一个对象而不是原始值。比如下面,比起将用户名、电子邮件和设置一个一个地传下去不如使用一个对象。这也减少了用户获得额外字段时需要做的更改。

使用 TypeScript 进行类型注解会让这变得更容易

// 👎 Don't pass values on by one if they're related
<UserProfile
bio={user.bio}
name={user.name}
email={user.email}
subscription={user.subscription}
/>

// 👍 Use an object that holds all of them instead
<UserProfile user={user} />

条件渲染

在某些情况下,为条件呈现使用短路操作符(&&)可能会适得其反,您可能最终在 UI 中得到一个不想要的 0。

为了避免这种情况,默认使用三元运算符。唯一的警告是,它们更加冗长。

短路操作符减少了代码的数量,这总是很好。三进制更冗长,但不可能出错。另外,添加替代条件的改变更小。

// 👎 Try to avoid short-circuit operators
function Component() {
const count = 0;

return <div>{count && <h1>Messages: {count}</h1>}</div>;
}

// 👍 Use a ternary instead
function Component() {
const count = 0;

return <div>{count ? <h1>Messages: {count}</h1> : null}</div>;
}

避免嵌套的三元运算符

三值运算符在第一级之后变得难以阅读。即使它们在当时看起来节省了空间,但最好是能够一眼看出代码块的意图

// 👎 Nested ternaries are hard to read in JSX
isSubscribed ? (
<ArticleRecommendations />
) : isRegistered ? (
<SubscribeCallToAction />
) : (
<RegisterCallToAction />
);

// 👍 Place them inside a component on their own
function CallToActionWidget({ subscribed, registered }) {
if (subscribed) {
return <ArticleRecommendations />;
}

if (registered) {
return <SubscribeCallToAction />;
}

return <RegisterCallToAction />;
}

function Component() {
return <CallToActionWidget subscribed={subscribed} registered={registered} />;
}

将递归渲染的 DOM 封装为单独的组件

循环遍历项列表是一种常见的操作,通常使用 map 函数完成。然而,在一个有很多标记的组件中,额外的缩进和 map 语法对可读性没有帮助。

当您需要映射元素时,在它们自己的列表组件中提取它们,哪怕标记并不多。父组件不需要知道详细信息,只需要知道它正在显示一个列表。

只有当组件的主要职责是显示它时,才在标记中保持一个循环。尽量为每个组件保留一个映射,但如果标记很长或很复杂,可以用任何一种方法提取列表。

// 👎 Don't write loops together with the rest of the markup
function Component({ topic, page, articles, onNextPage }) {
return (
<div>
<h1>{topic}</h1>
{articles.map((article) => (
<div>
<h3>{article.title}</h3>
<p>{article.teaser}</p>
<img src={article.image} />
</div>
))}
<div>You are on page {page}</div>
<button onClick={onNextPage}>Next</button>
</div>
);
}

// 👍 Extract the list in its own component
function Component({ topic, page, articles, onNextPage }) {
return (
<div>
<h1>{topic}</h1>
<ArticlesList articles={articles} />
<div>You are on page {page}</div>
<button onClick={onNextPage}>Next</button>
</div>
);
}

解构时为 props 设置默认值

指定默认 props 值的一种方法是将 defaultProps 属性附加到组件上。这意味着组件函数及其参数的值不会放在一起。

最好在重新构造 props 时直接分配默认值。它使从上到下阅读代码变得更容易,而无需跳转,并将定义和值保持在一起。

// 👎 Don't define the default props outside of the function
function Component({ title, tags, subscribed }) {
return <div>...</div>;
}

Component.defaultProps = {
title: "",
tags: [],
subscribed: false,
};

// 👍 Place them in the arguments list
function Component({ title = "", tags = [], subscribed = false }) {
return <div>...</div>;
}

避免嵌套的组件

当您需要从组件或逻辑中提取标记时,不要将其放在位于同一组件中的函数中。组件只是一个函数。这样定义它就是嵌套在其父类中。

这意味着它可以访问其父节点的所有状态和数据。它使代码更加不具有可读性——这个函数在所有组件之间的作用是什么?

将其移动到自己的组件中,命名并依赖于 props 而不是 closure。

// 👎 Don't write nested render functions
function Component() {
function renderHeader() {
return <header>...</header>;
}
return <div>{renderHeader()}</div>;
}

// 👍 Extract it in its own component
import Header from "@modules/common/components/Header";

function Component() {
return (
<div>
<Header />
</div>
);
}

状态管理

使用 Reducers

有时您需要一种更强大的方式来传递和管理状态更改。在使用外部库之前先使用 useReducer。这是一个很好的机制来进行复杂的状态管理,它不需要第三方依赖。

结合 React s Context 和 TypeScript, useReducer 可以变得非常强大。不幸的是,它并没有被广泛使用。人们仍然使用第三方库。

如果您需要多个状态块,则将它们移动到 Reducers。

// 👎 Don't use too many separate pieces of state
const TYPES = {
SMALL: 'small',
MEDIUM: 'medium',
LARGE: 'large'
}

function Component() {
const [isOpen, setIsOpen] = useState(false)
const [type, setType] = useState(TYPES.LARGE)
const [phone, setPhone] = useState('')
const [email, setEmail] = useState('')
const [error, setError] = useSatte(null)

return (
...
)
}

// 👍 Unify them in a reducer instead
const TYPES = {
SMALL: 'small',
MEDIUM: 'medium',
LARGE: 'large'
}

const initialState = {
isOpen: false,
type: TYPES.LARGE,
phone: '',
email: '',
error: null
}

const reducer = (state, action) => {
switch (action.type) {
...
default:
return state
}
}

function Component() {
const [state, dispatch] = useReducer(reducer, initialState)

return (
...
)
}

优先选择 hooks 而不是 HOC 或者 render props

在某些情况下,我们需要增强组件或允许它访问外部状态。通常有三种方法可以做到这一点-高阶组件(HOCs),props 和 hooks。

hooks 已经被证明是实现这种组合的最有效的方法。从哲学的角度来看,组件是使用其他功能的功能。hooks 允许您访问外部功能的多个来源,而不会相互冲突。不管有多少个 hooks,您都知道每个值来自哪里。

使用第三方数据获取插件

我们想要管理状态的数据通常是从 API 中检索的。我们需要将数据保存在内存中,更新它,并在多个地方访问它。像 React Query 这样的现代数据获取库提供了足够的机制来管理外部数据。我们可以缓存它,使它失效,并重新获取它。它们还可以用于发送数据,触发另一条数据的刷新。如果使用像 Apollo 这样的 GraphQL 客户端,则更容易。它内置了客户端状态的概念。

状态管理插件

在大多数情况下,您不需要状态管理库。它们应该用于需要管理复杂状态的大型应用程序。关于这个主题有很多指南,所以我将只提到在这种情况下我将探索的两个库——Recoil 和 Redux。

组件心智模式

Container & Presentational (容器和展示组件)

主要的思路是将组件分成两组:表示组件和容器组件。也被称为聪明和愚蠢。其思想是,有些组件没有任何功能和状态。它们只是被带有一些道具的父组件调用。容器组件包含业务逻辑,执行数据获取和管理状态。这种心理模型就是 MVC 结构对于后端应用程序的作用。它足够通用,可以在任何地方工作,你不会错的。但是,在现代 UI 应用程序中,这种模式还不够。把所有的逻辑都拉进来。

无状态和有状态组件

可以将组件分为有状态和无状态两种。上面提到的心智模型意味着一些组件应该管理大量的复杂性数据。相反,它应该在整个应用程序内使用。

数据应该位于使用数据的地方附近。当您使用 GraphQL 客户端时,您将在显示数据的组件中获取数据。即使它不是顶级的。不要想容器,想想组件的职责。考虑保存状态块的最符合逻辑的组件是什么。

例如,一个 <Form /> 组件应该有属于自己的数据,一个 <Input /> 在输入值改变时应该接收值并进行回调。当一个<Button /> 组件被点击时应该通知表单并让表单知道发生了什么。

谁在表单中进行验证?输入字段负责吗?这意味着该组件将意识到应用程序的业务逻辑。它如何通知表单有错误?如何刷新错误状态?表格会知道吗?如果有一个错误,但你试图提交,会发生什么?

当面对这样的问题时,你应该意识到组件的职责正在被混淆。在这种情况下,最好让输入保持无状态,并从表单接收错误消息。

应用架构

通过路由或者模块分组

按容器和组件分组使应用程序难以导航。要理解组件属于哪里,您需要非常熟悉整个应用。

并不是所有的组件都是相同的——有些是全局使用的,有些是为应用程序的特定部分制作的。这种结构对于最小的项目很有效。但是,对于大型项目来说很难管理。

// 👎 Don't group by technical details
├── containers
| ├── Dashboard.jsx
| ├── Details.jsx
├── components
| ├── Table.jsx
| ├── Form.jsx
| ├── Button.jsx
| ├── Input.jsx
| ├── Sidebar.jsx
| ├── ItemCard.jsx

// 👍 Group by module/domain
├── modules
| ├── common
| | ├── components
| | | ├── Button.jsx
| | | ├── Input.jsx
| ├── dashboard
| | ├── components
| | | ├── Table.jsx
| | | ├── Sidebar.jsx
| ├── details
| | ├── components
| | | ├── Form.jsx
| | | ├── ItemCard.jsx

开始使用 路由或者模块来进行目录的组织,这是一个支持变化和增长的结构。关键是不要让您的应用程序快速超出体系结构。如果你的项目是基于组件和容器的,那将会很快就会变得无序和难于查找。

基于组件和容器的结构没有错,但太一般化了。除了它使用 React 之外,它没有告诉读者任何关于这个项目的信息。

创建公共的模块

像按钮、输入和卡片这样的组件随处可见。即使你不打算使用基于模块的结构,提取这些也是很好的。即使你不使用 Storybook,你也可以看到你所拥有的公共组件。它有助于避免重复。你不希望团队中的每个人都制作自己版本的按钮。不幸的是,由于结构糟糕的项目,这种情况经常发生。

使用绝对路径

使事情更容易更改是项目结构的基础。绝对路径意味着如果你需要移动一个组件,你需要改变的就少了。同时,它也让我们更容易找到所有东西的来源。

// 👎 Don't use relative paths
import Input from "../../../modules/common/components/Input";

// 👍 Absolute ones don't change
import Input from "@modules/common/components/Input";

我使用@前缀表示它是一个内部模块,但我也看到过使用~完成它

对外部组件进行包装

尽量不要直接导入太多第三方组件。通过围绕它们创建适配器,我们可以在必要时修改 API。此外,我们可以在一个地方改变第三方库。

这同样适用于语义 UI 和实用程序组件等组件库。您可以做的最简单的事情是从公共模块重新导出它们,以便从相同的位置提取它们。

组件不需要知道我们使用什么库作为日期选择器——只需要知道它存在即可。

// 👎 Don't import directly
import { Button } from "semantic-ui-react";
import DatePicker from "react-datepicker";

// 👍 Export the component and use it referencing your internal module
import { Button, DatePicker } from "@modules/common/components";

为组件建立单独的文件夹

我为 React 应用程序中的每个模块创建了一个组件文件夹。每当我需要创建一个组件时,我都会先在这里创建它。如果它需要额外的文件,如样式或测试,我创建自己的文件夹并将它们放在那里。

作为一种常规做法,最好有一个 index.js 文件来导出 React 组件,这样你就不必更改导入路径或重复导入路径, 比如像import Form from 'components/UserForm/UserForm'。不过,要保留组件文件的名称,这样当打开多个组件时就不会混淆了。

// 👎 Don't keep all component files together
├── components
├── Header.jsx
├── Header.scss
├── Header.test.jsx
├── Footer.jsx
├── Footer.scss
├── Footer.test.jsx

// 👍 Move them in their own folder
├── components
├── Header
├── index.js
├── Header.jsx
├── Header.scss
├── Header.test.jsx
├── Footer
├── index.js
├── Footer.jsx
├── Footer.scss
├── Footer.test.jsx

性能

不要过早的开始性能优化

在进行任何类型的优化之前,请确保这些优化是有原因的。盲目地遵循最佳实践是浪费精力,除非它在某种程度上影响了您的应用程序。

是的,知道某些事情是很重要的,但是在性能之前先构建可读和可维护的组件。编写好的代码更容易改进。

当您注意到应用程序中的性能问题时——测量并确定问题的原因。如果你项目打包之后非常大,那么就没有必要减少渲染数量

一旦您知道了性能问题来自哪里,就按照其影响的重要性修复它们。

观察引入包的大小

发送到浏览器的 JavaScript 数量是影响应用程序性能的最重要因素。你的应用程序可以非常快,但如果他们需要加载 4MB 的 JS 才能加载它,可能没有人会发现这一点。

不要发布单一的 JS 包。在路由级别上拆分应用程序,甚至更进一步。确保你发送的 JS 数量尽可能少。

在后台加载或者当用户显示他们需要另一个 bundle 的意图时。如果按下一个按钮会触发 PDF 下载,您可以延迟 PDF 库的下载,直到按钮悬停。

减少重复渲染 - 回调,数组和对象

试着减少应用中不必要的渲染量是很好的。请记住这一点,但也要注意,不必要的渲染很少会对你的应用产生最大的影响。

最常见的建议是避免传递回调函数作为 props。使用一个表示每次都会创建一个新函数,从而触发一个渲染器。我没有遇到任何回调相关的性能问题,事实上,这是我的首选方法。

如果遇到性能问题,而闭包是罪魁祸首的话,则删除它们。但是不要使你的代码可读性降低,也不要使它变得不必要的冗长。

直接向下传递数组或对象属于同一类问题。它们没有通过引用检查,因此将触发一个渲染器。如果需要传递固定数组,则在组件定义之前将其作为常量提取,以确保每次都传递相同的实例。

测试

不要依赖快照测试

自从 2016 年我开始使用 React 以来,我只遇到过一次快照测试在我的组件中发现问题的情况。没有参数的 new Date()调用已经溜走,它总是默认为当前日期。

除此之外,快照只是在组件更改时导致构建失败的原因。通常的工作流程是对组件进行更改,查看快照是否失败,更新它们,然后继续。

不要误解我的意思,它们是一种很好的完整性检查,但它们不能替代好的组件级测试。我甚至不再使用它们。

测试正确展示

测试应该验证的主要内容是组件是否按预期工作。确保它使用 default props 和传递给它的 props 正确地呈现。验证对于给定的输入(props)函数是否返回正确的结果(JSX)。确认您需要的所有内容都展示在屏幕上。

验证状态和事件

有状态组件很可能会随着事件的响应而更改。模拟事件,并确保组件正确响应这些事件。验证是否调用了处理函数并传递了正确的参数。检查内部状态是否设置正确。

测试边界情况

当你有了基本的测试,确保你添加一些处理边缘情况。这意味着传递一个空数组,以确保您不会在没有检查的情况下访问索引。在 API 调用中抛出一个错误以确保组件处理它。

编写集成测试

集成测试是为了验证整个页面或更大的组件。它测试它作为一个抽象是否有效。它们使我们对应用程序按预期工作最有信心。组件本身可以工作得很好,并且它们的单元测试可以通过。不过,它们之间的整合可能会出现问题。

样式

使用 css-in-js

这是一个很有争议的观点,很多人都不同意。我宁愿使用像样式组件或 Emotion 这样的库,因为它允许我用 JavaScript 来表达我的组件的所有内容。少维护一个文件。没有需要考虑的 CSS 约定。React 中的逻辑单元是组件,所以从关注点分离的角度来看,它应该拥有与它相关的所有东西。注意:当谈到样式时没有错误的选择- SCSS, CSS 模块,像 Tailwind 这样的库。CSS-in-JS 就是我推荐的方法。

保持统一的组件书写格式

当涉及到 CSS-in-JS 组件时,在同一个文件中有多个是正常的。理想情况下,我们希望将它们保存在与使用它们的常规组件相同的文件中。但是,如果样式变得太长,可以将它们提取到使用它们的组件旁边的自己的文件中。我曾在 Spectrum 等开源项目中看到过这种模式。

数据获取

使用一个获取数据的第三方包

React 不提供从 API 获取或更新数据的武断方式。每个团队创建自己的实现,通常涉及一个包含与 API 通信的异步函数的服务。这意味着我们需要自己管理加载状态和处理 http 错误。这将导致使用大量样板文件的冗长代码。

相反,我们应该使用类似 React Query 或 SWR 这样的库。它们以一种惯用的方式——钩子——使与服务器的通信成为组件生命周期的自然组成部分。它们内置了缓存,为我们管理加载和错误状态。我们只需要处理他们。此外,它们还消除了使用状态管理库来处理数据的需要。