查看原文
其他

【第2777期】React最新提出了一个名为use的Hook

ikoofe 前端早读课 2022-11-10

前言

消失两天,回来了。了解 React 新动态。今日前端早读课文章由 @ikoofe 翻译分享,公号:KooFE 前端团队授权。

正文从这开始~~

React 最新提出了一个叫 use 的 Hook,用于在客户端消费 Promise,而 use 和其他 Hook 不同之处在于,它可以在条件语句、block 和循环里使用。除 Promise 之外,use 未来还可能用于消费 Context、store、observable 等。

与之对应的是,在服务端组件中则支持使用 async/await。这个提案本身并不是一个完整的数据获取解决方案,因为它没有解决缓存问题。wait 和 use 是用于读取 promise 异步结果的原语,后续会引入一个名为 cache 的 API 来帮助缓存数据。

示例 1:在 Server Components 使用 await

在 Server Components 中可以使用标准的 async/await 语法来访问基于 promise 的 APIs。并不需要额外的处理:

// This example was adapted from the original Server Components RFC:
// https://github.com/reactjs/rfcs/pull/188
async function Note({id, isEditing}) {
const note = await db.posts.get(id);
return (
<div>
<h1>{note.title}</h1>
<section>{note.body}</section>
{isEditing ? <NoteEditor note={note} /> : null}
</div>
);
}

在异步 Server Components 中,使用 Hook 是受限制的。因为 Server Components 是无状态的,在这种场景下 Hook 基本是无用的。尽管这样,我们还是可以像写一些正常函数那样,在 Server Components 中使用一些特定的 Hooks(比如,useId)。

示例 2:在 Client Components 和 Hooks 中使用 use

由于技术限制,在 client 端渲染的组件并不支持 await。于是 React 提供了一个特殊的 Hook -- use。我们可以把 use 看作是一个 React 版本的 await。正如 await 只能被用于异步函数中,use 也只能用于 React 组件和 Hooks 内部:

// `use` inside of a React component or Hook...
const data = use(promise);

// ...roughly equates to `await` in an async function
const data = await promise;

use 有一点比较特殊,是其他 Hooks 所不具备的:它能在条件语句、代码块以及循环语句里使用。这样,就可以根据条件来加载数据,而不需要将数据处理逻辑放置于独立的组件中。

function Note({id, shouldIncludeAuthor}) {
const note = use(fetchNote(id));

let byline = null;
if (shouldIncludeAuthor) {
const author = use(fetchNoteAuthor(note.authorId));
byline = <h2>{author.displayName}</h2>;
}

return (
<div>
<h1>{note.title}</h1>
{byline}
<section>{note.body}</section>
</div>
);
}

当然,use 不仅仅用于 promise,未来 use 也会支持除 Promise 之外的其他场景,比如 Context 等:

const resolvedValue = use(promise);
const contextualValue = use(Context);

// Potential future Usable types
// (Purely hypothetical, not part of this proposal)
const currentState = use(store);
const latestValue = use(observable);
// ...and so on

为什么引入 use?

在文档「First class support for promises and async/await」中详细介绍了 use 出现的背景和原因,下面这部分内容主要是对该文做的翻译,以方便大家更好地了解 use。

与 JavaScript 生态的无缝集成

其中一个最主要的出发点就是能够与其它 JavaScript 生态无缝对接,promise 已经成为在 JavaScript 异步场景中的常用方法。

然而,Server Components 最初的提案不能很好地使用基于 promise 的 APIs。需要为每个数据源做额外的处理,而且这些工作很容易出错,导致使用这些异步操作的方式并不直观。

而且大家也经常提出这样的疑问:为什么不能使用 async/await?React 提供了这样的回答:

React 更倾向于在 Server Components、Client Components 和 Shared Components 中使用一套 API 来访问数据。

这句话隐含的意思是,由于技术所限在 Client Components 支持 async/await 是做不到的。

确实,在理想情况下,在任何地方 async/await 都应被支持。我们也不希望用我们自己的异步原语来替换 promise,也不想让每个异步 API 都要被 React 定制化处理。同时,在最初的 RFC 中,我们认为在 Server 和 Client Components 有一套一致的 API 是更重要的目标。

我们已经改变了想法。现在我们坚信在 Server Components 中使用 async/await 的收益已经远超过与 client 的 API 保持一致(所以,在 client 使用了 use)。

原因之一就是我们低估了 Server Components 需要专门处理异步操作的频率。在 JavaScript 服务端应用中基于 promise 的 API 十分普遍,导致不得不到处做 React 包装和绑定。这也是我们原来提案备受批评的原因之一。

我们意识到,为了在 Server 端和 Client 端组件之间提供一致的 API,而实际上我们并没有消除对 async/await 的需求,开发人员在 React 组件之外执行逻辑时仍然需要使用 async/await,比如提交表单和处理服务器响应。

避免在 server 和 client 端产生恐怖谷效应

尽管我们最初犹豫不决,但在 server 端和 client 端上使用不同的数据访问方式也有一个优势:这使得跟踪他们工作的环境更加容易。

Server Components 想要与 Client Components 相似的感觉,但我们也不希望它们太相似。每个环境都有不同的功能,开发人员在构建应用程序时,需要清楚了解这些差异。例如,在 Server Components 中通常要完成尽可能多的事情,并且按需提供给 Client Components 尽可能少的内容。

让 Server Components 和 Client Components 更容易区别,将有助于避免产生恐怖谷效应,让开发人员花费更少的精力来辨别哪些组件在哪个环境中运行。

async 函数提供了一个清晰的视觉信号:如果组件是作为异步函数编写的,那就是服务器组件。

即使在将来,如果我们能够在客户端上添加对异步组件的支持,我们可能会继续建议在异步组件(即那些获取数据的组件)和有状态组件(即使用挂钩的组件)之间进行分离,以便于重构。一个常见的工作流可能是将一个既获取数据又使用状态的组件重构为多个分别处理获取和状态的组件,然后再进一步将获取移动到服务器。

避免数据的取和读耦合在一起

await 和 use 并不干涉 promise 中的数据请求,它们只关注于异步数据的包装,而不关心数据是如何获取的。

在之前的 Suspense 数据获取 API 中,由于没有内置的读异步数据的方法,导致数据获取和渲染耦合在一起。当时的想法是,每个数据源都应该提供 preload 和 read 方法,前者用于数据获取,后者用于读取数据,但是这种做法只是纯粹的遵循惯例,无法保证两种方法的一致性。

通过建设读取异步数据标准方案,我们希望将数据获取和渲染解耦。

举个例子,一种常见的提高性能的开发模式是在不阻塞当前渲染的情况下,将未来要渲染的数据进行提前加载。无论你用什么类库去获取数据,use 可以让这种实现更直接:

function TooltipContainer({showTooltip}) {
// This is a non-blocking fetch. We initiated the request but we haven't
// yet unwrapped the result.
const promise = fetchInfo();

if (!showTooltip) {
// If `showTooltip` is false, we can return immediately without waiting
// for the data to finish loading.
return null;
} else {
// If `showTooltip` is true, we wait for the promise to resolve by passing
// it to `use`. It probably already loaded, because the request was
// initiated during the previous render.
return <Tooltip content={use(promise)} />;
}
}
降低 React 中的复杂性

在过去,我们不愿意为数据获取增加新的官方 API,因为我们不想将开发者锁定到一个不是那么优秀的架构中。数据获取是一个比较复杂的事情,需要做许多权衡和取舍,最好的解决方案是能够与应用架构的其它部分深度集成 - 比如路由。

然而,实现 React 的应用架构并不是唯一的,整个 React 生态都受益于第三方类库和框架的创新能力。如果 React 对数据获取做了太多假设,就会导致用户无法将一些最方案用于实践。但是从另一个方面来说,如果 React 什么都不做,我们就丧失了改善开发者体验的能力。

这个提案的目标是,为数据获取提供一套可共享的基础原语,适用于各种不同的 React 框架,不对如何进行数据获取做过多限制。无论使用框架与否,还是切换各种框架,组织 React 的代码和模式是不会变化的。举个例子,Next.js 和 Remix 非常有影响力的两个 React 框架,它们的路由和缓存策略或许完全不同,但是它们不需要为 loading 状态、条件 loading 和错误处理重新发明 API,它们可以使用 React 提供的模式或方法。

另一个目的是,让开发人员更容易在简单和复杂的场景之间进行扩展。随着应用程序变得越来越复杂,你需要将组件逐步升级,提供更高级的功能。与之类似,如果你已经在使用一个复杂的数据框架,也应该能够在不影响整个系统架构的情况下,向单个组件中添加简单的一次性数据获取。

启用基于编译的优化

因为 async/await 是语法特性,很适合针对编译过程进行优化。在未来,我们可以将异步 Server Components 编译成较低级别类似于生成器的形式,以减少诸如微任务之类的运行时开销,而且不会影响组件的外部行为。尽管在 JavaScript 中,use 在技术上不是一种语法结构,但它在 React 应用程序的上下文中可作为语法(我们将使用 linter 来强制正确使用),因此我们也可以在 client 应用类似的编译器优化。

我们在整个设计过程中都认真考虑了这一点。例如,在当前版本的 React 中,允许任意函数在渲染期间通过抛出 promise 来挂起是不稳定的。有了 use 之后,我们将在以后的版本中删除该机制。这意味着只允许 Hooks 挂起。编译器可以利用这些知识来防止函数进行不必要、无效的计算。

关于本文
译者:@ikoofe
译文:https://mp.weixin.qq.com/s/4jBYUElwNg-VzXavG-EWkg
原文:https://github.com/reactwg/server-components/discussions/2

关于【Hook】相关推荐,欢迎读者自荐投稿,前端早读课等你来。+v:zhgb_f2er

【第2510期】不优雅的 React Hooks

【第2232期】深入理解React Router:Context、Hooks、Refs、Memo特性讲解

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存