Menu

React use Hook 教程:在渲染中读取 Promise 和 Context

use 在渲染期间读取 Promise 或 context 的值。读取 Promise 时,它会挂起直到数据到达,并显示最近的 Suspense 后备内容。与其他 Hook 不同,它可以在 if 语句和循环中运行。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

use 是 React 19 的一个 API,在组件渲染时从 Promise 或 context 中读取值。use(promise) 会让组件暂停,直到 Promise resolve,期间显示最近的 <Suspense> 后备内容;use(context) 的作用和 useContext 相同,但可以放在 if 或循环里。

下面的 fetchUser 函数代替了真实的请求。

第一次渲染会先显示一秒“Loading...”,然后显示用户资料。点击 User 2:新的 Promise 处于 pending 状态时,后备内容又会出现。Profile 没有自己的加载状态;它写得就好像数据已经在那里了一样。

use(promise) 的工作原理

当 Profile 调用 use(userPromise) 时:

  • 如果 Promise 已经 resolve,use 返回它的值,渲染继续进行。
  • 如果它仍处于 pending 状态,React 会停止渲染 Profile,并显示它上方最近的 <Suspense> 的后备内容。等 Promise 有了结果,React 会再次渲染 Profile,这次 use 就会返回值。
  • 如果它被 reject,React 会改为显示最近的错误边界。

React 把结果记在 Promise 对象本身上。这就是为什么下一次渲染时,Promise 必须是同一个对象。

想在下一个用户资料加载期间继续显示旧的资料,就在 transition 中设置 Promise。这样 React 会等待,而不会再次显示后备内容:

<button onClick={() => startTransition(() => setUserPromise(fetchUser(2)))}>User 2</button>

Promise 应该在哪里创建

这是最容易出错的地方。在读取数据的组件里直接写请求调用,看起来很自然,却行不通:

// Do not do this
function Profile({ id }) {
    const user = use(fetchUser(id)); // a new promise on every render
    return <p>{user.name}</p>;
}

每次渲染都会调用 fetchUser,得到一个新的、处于 pending 状态的 Promise。Profile 挂起。等这个 Promise resolve,React 再次渲染 Profile,又创建了另一个 pending 的 Promise,于是再次挂起。组件永远显示不出数据,网络面板里的请求则越积越多。

要把 Promise 创建在不会随读取它的组件每次渲染而重新执行的地方:

  • 在父组件中,保存在 state 里(就像第一个示例那样),或者在事件处理函数中创建。
  • 在模块顶层,适用于数据不依赖 props 的情况:在组件上方写 const configPromise = fetchConfig();。
  • 在缓存中,以参数作为键,这样同一个 id 会返回同一个 Promise。
  • 在框架中:Server Component 可以把 Promise 作为 prop 传给客户端组件,路由 loader 也是如此。这正是 use 的设计初衷。

下面是缓存版本,组件可以按 id 请求数据,同时拿到一个稳定的 Promise:

依次点击三个城市,再点回一个已经打开过的城市。控制台对每个城市只记录一次请求,而之前看过的城市会直接显示,不经过后备内容,因为它的 Promise 早已 resolve。删掉 if (!cache.has(city)) 这个检查,组件就又会永远挂起。

真正的缓存还需要让条目过期的办法。数据请求库和框架会处理这件事,这也是为什么大多数应用的 Promise 都来自它们。

use(context),甚至可以放在 if 里

use(SomeContext) 返回的值和 useContext(SomeContext) 相同。区别在于可以在哪里调用它。其他所有 Hook 都必须在顶层运行,并且每次渲染的顺序都相同(见 Hook 规则)。而 use 可以在提前 return 之后、在条件里或在循环里调用。

点击按钮,在两个分支之间切换。把 use(UserContext) 换成 useContext(UserContext),代码就违反了 Hook 规则,因为这个 Hook 会在某些渲染中运行,在另一些渲染中不运行。

use 仍然有限制:只能在渲染组件时或在自定义 Hook 中调用它。在事件处理函数或 Effect 里,要读取 context,就用渲染期间已经拿到的值。

错误处理:用错误边界,而不是 try/catch

当 Promise 被 reject 时,错误会交给最近的错误边界,和渲染错误的处理方式一样。

打开 Post 2,错误边界会显示错误信息。再打开 Post 1,它又能正常显示了:key={id} 让错误边界为每篇文章都使用全新的 state,所以它不会一直显示旧的错误。

你无法用 try/catch 捕获处于 pending 状态或被 reject 的 use。Promise 处于 pending 状态时,use 会通过抛出一个特殊的值来中断渲染,包在外面的 catch 会把它吞掉。捕获到的值是一个错误,其信息以“Suspense Exception: This is not a real error!”开头,而且在开发环境中,React 还会打印“use was called from inside a try/catch block”。如果想显示一个后备值而不是错误界面,就在把 Promise 往下传之前处理它的 reject:

const postPromise = fetchPost(id).catch(() => 'This post could not be loaded.');

use 与在 useEffect 中请求数据的对比

经典的客户端写法是在 Effect 中请求数据,并维护三份 state:

function Profile({ id }) {
    const [user, setUser] = useState(null);
    const [error, setError] = useState(null);

    useEffect(() => {
        let ignore = false;
        fetchUser(id).then(
            (u) => !ignore && setUser(u),
            (e) => !ignore && setError(e),
        );
        return () => {
            ignore = true;
        };
    }, [id]);

    if (error) return <p>Error</p>;
    if (!user) return <p>Loading...</p>;
    return <p>{user.name}</p>;
}
useEffectuse
请求何时开始第一次渲染显示到屏幕之后Promise 创建时,可能在组件渲染之前
加载状态你自己写的 if (!user)最近的 <Suspense>
错误你自己的 error state最近的错误边界
竞态条件由你来防范(ignore)组件始终读取交给它的那个 Promise
由谁创建 Promise组件自己父组件、缓存或框架

Effect 版本是自成一体的,这也是它在没有框架的应用中仍然最常见的原因;请求数据页面对它有完整的介绍。当组件上层的某个地方提前发起请求、再把 Promise 传下来时,use 才能真正发挥优势。

常见问题

React 中的 use Hook 有什么作用?

use(resource) 返回一个 Promise 或 context 的值。传入 Promise 时,组件会挂起直到它 resolve,期间最近的 <Suspense> 显示后备内容;如果它 reject,则改为显示最近的错误边界。

use 可以有条件地调用吗?

可以。use 是唯一一个可以在 if 语句、循环中以及提前 return 之后调用的 Hook。不过它仍然必须在组件或 Hook 中调用,绝不能在事件处理函数或 Effect 中调用。

为什么在组件里写 use(fetch()) 会无限循环?

每次渲染都会创建一个新的 Promise。组件在它上面挂起,Promise resolve 后 React 再次渲染,而这次渲染又创建了另一个处于 pending 状态的 Promise。应该在渲染之外创建 Promise:放在父组件的 state 中、模块顶层、缓存里,或者框架的 loader 中。

可以用 try/catch 包住 use 吗?

不可以。Promise 处于 pending 状态时,use 会通过抛出来中断渲染,捕获它就会破坏这个机制。被 reject 的 Promise 要用错误边界来处理,或者给 Promise 加上 .catch(),让它 resolve 成一个后备值。

应该用 use 取代 useEffect 里的数据请求吗?

use 需要一个只创建一次、可以重复使用的 Promise,而这正是框架和数据请求库提供的。在普通的客户端应用里,在 Effect 中请求数据(或使用 TanStack Query 这样的库)仍然很常见,也完全没问题。

Coddy 编程语言插图

和 Coddy 一起学编程

开始