在前面一篇文中,我们知道,如果直接使用 use 获取未直接 resolve 的 Promise 中的值,会抛出一个异常。

1
const _api3 = () => {
2
return new Promise(resolve => {
3
resolve({ value: '_api3' })
4
})
5
}
6
7
// bad: get an error
8
const result = use(_api3())

在实践中,大多数情况都是这种并没有直接得到 Promise resolve 的结果状态,那我们应该怎么办呢?这个时候我们可以利用 Suspense 来解决这个问题。

1Suspense

Suspense 可以捕获 use 无法读取到数据时抛出的异常,然后此时会在页面上渲染回退组件 fallback

1
<Suspense fallback={<Skeleton />}>
2
<Message promise={promise} />
3
</Suspense>

接下来看一个简单的演示案例。在这个例子中,为了让 Suspense 捕获更小范围的组件,我们单独定义了一个子组件 Message 来使用 use 获取 promise 中的数据。

预览
index.tsx
message.tsx
api.ts
1
import { Suspense } from 'react'
2
import Message from './message'
3
import Skeleton from 'components/ui/skeleton'
4
import { getMessage } from './api'
5
6
export default function Page() {
7
const promise = getMessage()
8
return (
9
<Suspense fallback={<Skeleton />}>
10
<Message promise={promise} />
11
</Suspense>
12
)
13
}
14

2Suspense + use 读取异步接口请求数据

在开发中更常见的场景是使用 use 读取异步 promise,主要就是接口请求

1
<Suspense fallback={<Loading />}>
2
<Albums />
3
</Suspense>

在 React 19 中,use(promise) 被设计成完全符合 Suspense 规范的 hook,因此我们可以轻松的结合他们两者来完成页面开发。当 use(promise) 读取数据失败时,会抛出一个异常交给 Suspense 捕获,此时 Suspense 会渲染 fallback 回退组件。当请求成功之后,组件会重新渲染,此时 use(promise) 则可以读取到正确的值。

我们来梳理一下代码流程。

首先,我们定义好一个用于接口请求的函数,该函数执行返回 promise

api.ts
1
import { createRandomMessage } from '@/utils';
2
3
var requestOptions: RequestInit = {
4
method: 'GET',
5
redirect: 'follow'
6
};
7
8
const url = 'https://randomuser.me/api/?results=2&inc=name,gender,email,nat,picture&noinfo'
9
10
export async function getMessage() {
11
await fetch(url, requestOptions)
12
return { value: createRandomMessage()}
13
}

然后我们定义一个子组件 Message,该子组件接受一个 promise 作为参数。然后在子组件内部,我们使用 use 读取该 promise 中的值。

message.tsx
1
import { Tent } from 'lucide-react'
2
import { use } from 'react'
3
import { getMessage } from './api'
4
5
const Message = (props: { promise: ReturnType<typeof getMessage> }) => {
6
const message = use(props.promise);
7
return (
8
<div className='flex border border-gray-200 p-4 rounded items-start'>
9
<Tent />
10
<div className='flex-1 ml-3'>
11
<div>React introduction</div>
12
<div className='text-sm leading-6 mt-2 text-gray-600'>
13
{message.value}
14
</div>
15
</div>
16
</div>
17
)
18
}
19
20
export default Message

有了这个子组件之后,我们使用 Suspense 包裹捕获该组件的错误,防止错误溢出到更高层级的组件。

index.tsx
1
import { Suspense } from 'react'
2
import Skeleton from 'components/ui/skeleton'
3
import Message from './message'
4
import { getMessage } from './api'
5
6
export default function Page() {
7
const promise = getMessage()
8
return (
9
<Suspense fallback={<Skeleton />}>
10
<Message promise={promise} />
11
</Suspense>
12
)
13
}
14

完整代码及演示

预览
index.tsx
message.tsx
api.ts
1
import { Suspense } from 'react'
2
import Skeleton from 'components/ui/skeleton'
3
import Message from './message'
4
import { getMessage } from './api'
5
6
export default function Page() {
7
const promise = getMessage()
8
return (
9
<Suspense fallback={<Skeleton />}>
10
<Message promise={promise} />
11
</Suspense>
12
)
13
}
14

当 Message 组件首次渲染时,由于直接读取 promise 导致报错,Suspense 捕获到该异常后,会渲染 fallback 中设置的组件。此时我们设置了一个骨架屏 Skeleton 组件,大家可以多次点击 reload 按钮查看演示效果。

因此,这个案例的视觉表现应该为:首先渲染 Skeleton 组件。然后请求成功之后,渲染 Message 组件。

3Suspense 工作原理

Suspense 提供了一个加载数据的标准。在源码中,Suspense 的子组件被称为 primary

当 react 在 beginWork 的过程中(diff 过程),遇到 Suspense 时,首先会尝试加载 primary 组件。如果 primary 组件只是一个普通组件,那么就顺利渲染完成。

如果 primary 组件是一个包含了 use 读取异步 promise 的组件,它会在首次渲染时,抛出一个异常。react 捕获到该异常之后,发现是一个我们在语法中约定好的 promise,那么就会将其 then 的回调函数保存下来,并将下一个 next beginWork 的组件重新指定为 Suspense

此时 promise 在请求阶段,因此再次 beginWork Suspense 组件时,会跳过 primary 的执行而直接渲染 fallback

primary 中的 promise 执行完成时「resolve」,会执行刚才保存的 then 方法,此时会触发 Suspense 再次执行「调度一个更新任务」。由于此时 primary 中的 promise 已经 resolve,因此此时就可以拿到数据直接渲染 primary 组件。

整个流程可以简单表示为:

1
Suspense ->
2
primary ->
3
Suspense ->
4
fallback ->
5
waiting -> resolve() ->
6
Suspense ->
7
primary ->

4primary 为普通组件时

primary 为普通组件时,会直接渲染普通组件,如下案例所示。

预览
index.tsx
message.tsx
1
import React, { Suspense } from 'react';
2
import Skeleton from 'components/ui/skeleton'
3
import { createRandomMessage } from '@/utils'
4
import Message from './message'
5
6
export default function Demo03() {
7
return (
8
<Suspense fallback={<Skeleton />}>
9
<Message message={createRandomMessage()} />
10
</Suspense>
11
)
12
}
13

5新旧实现对比

在前面我们 结合 use 与 Suspense 实现了一个初始化加载的案例。该案例的视觉表现是在初始化时,首先显示 Skeleton 组件,请求成功之后,显示 Message 组件。

刷新页面时重新请求数据渲染,请求过程中显示骨架屏组件 Skeleton

核心代码与演示效果如下,点击刷新按钮重复观察执行效果

预览
index.tsx
message.tsx
api.ts
1
import { Suspense } from 'react'
2
import Skeleton from 'components/ui/skeleton'
3
import Message from './message'
4
import { getMessage } from './api'
5
6
export default function Page() {
7
const promise = getMessage()
8
return (
9
<Suspense fallback={<Skeleton />}>
10
<Message promise={promise} />
11
</Suspense>
12
)
13
}
14

这里我们需要关注的是,对比以前必须要借助 state useEffect 的实现方式,体会一下差别

预览
index.tsx
message.tsx
api.ts
1
import React, { useEffect, useState } from 'react'
2
import Skeleton from 'components/ui/skeleton'
3
import Message from './message'
4
import { getMessage } from './api'
5
6
export default function Demo04() {
7
const [content, update] = useState({ value: '' })
8
const [loading, setLoading] = useState(true)
9
10
useEffect(() => {
11
getMessage().then(res => {
12
update(res)
13
setLoading(false)
14
})
15
}, []);
16
17
if (loading) {
18
return <Skeleton />
19
}
20
21
return <Message message={content.value} />
22
}
23

可以很明显的看出,新的方式使用 use + Suspense ,代码更加简洁。

除此之外,在严格模式下,开发环境组件首次加载会执行两次,因此我们还需要想额外的办法防止重复执行,代码会变得更加冗余。一个很明显的差别就是 Suspense + use 的方式会自动帮助我们弃用第二次的请求数据。而使用 useEffect 则需要我们自己来处理防止重复请求的逻辑。

6总结

与老版本使用 state + useEffect 完成首页初始化的需求相比,新的开发方式更加的简洁,代码舒适度更高。

不过,在以前的开发方式中,我们可以通过自定义 hook 的方式,把状态与 useEffect 封装成自定义 hook.

useFetch.ts
1
function useFetch() {
2
const [content, update] = useState({value: ''})
3
const [loading, setLoading] = useState(true)
4
5
useEffect(() => {
6
api().then(res => {
7
setLoading(false)
8
update(res)
9
})
10
}, [])
11
12
return {content, loading}
13
}

最终在应用组件中也可以写出非常类似的非常简洁的代码。

index.tsx
1
function Index() {
2
const {content, loading} = useFetch()
3
4
if (loading) {
5
return <Skeleton />
6
}
7
8
return (
9
<Message message={content.value} />
10
)
11
}

这是我们之前版本的最佳实践。注意体会他们之间的区别。相似,但却不同。我们后续会列举更多案例,尽可能用新的开发思路去复现开发过程中会出现的场景。除此之外,Suspense 的实现方案,还能够更好的与并发 API 结合使用,这是老版本实现方案并不具备的优势,在后续的章节中我们会进一步学习。

专栏首页
到顶
专栏目录