Summary | 概述
数据加载异步化,不阻塞渲染,从而减少页面白屏时间
Motivation | 背景
DataLoader 提供了前置加载数据请求的能力,目前的设计是页面需要等待 DataLoader 的数据加载完成后,才开始渲染
如果接口过慢,可能导致白屏时间过久
因此,希望也能提供 DataLoader 不阻塞渲染的能力,页面可以先渲染,数据加载完成后,再次更新渲染内容
Usage example | 使用示例
见方案设计
Detailed design | 方案设计
根据对数据消费方式的差异,分为两套方案
方案1:
声明式的方式
- 在 defineDataLoader 时,通过 defer 标记数据请求不阻塞渲染
- 在消费数据时,使用 Suspense + Await 组件,声明数据请求各阶段的渲染内容
import { useData, defineDataLoader, Await } from 'ice';
import * as React from 'React';
export default function Home() {
const data = useData();
return (
<main>
<h1>Let's locate your package</h1>
<React.Suspense
fallback={<p>Loading package location...</p>}
>
<Await
resolve={data}
errorElement={
<p>Error loading package location!</p>
}
>
{(packageLocation) => (
<p>
Your package is at {packageLocation.latitude}
lat and {packageLocation.longitude} long.
</p>
)}
</Await>
</React.Suspense>
</main>
);
}
export const dataLoader = defineDataLoader(async () => {
const packageLocationPromise = getPackageLocation(
params.packageId,
);
return packageLocationPromise;
}, {
defer: true,
});
方案2:
进行状态判断
在 defineDataLoader 时,通过 defer 标记数据请求不阻塞渲染
使用 useAsyncData 来获取数据,返回值包含 3 项内容来标记请求状态,业务可通过条件判断来返回不同的内容
data 真实数据
error 请求的错误信息
isLoading 是否还在请求中
import { useAsyncData, defineDataLoader } from 'ice';
export default function Home() {
const { data, error, isLoading } = useAsyncData();
if (error) {
return <div>failed to load</div>;
}
if (isLoading) {
return <div>loading...</div>;
}
return (
<main>
<h1>Let's locate your package</h1>
<p>
Your package is at {data.latitude}
lat and {data.longitude} long.
</p>
</main>
);
}
export const dataLoader = defineDataLoader(async () => {
const packageLocationPromise = getPackageLocation(
params.packageId,
);
return packageLocationPromise;
}, {
defer: true,
});
如果 defineDataLoader 传入的是数组,useAsyncData 的返回值也对应一个数组,数组中的每一项状态同上
const [data1, data2] = useAsyncData();
const { data, error, isLoading } = data1;
Additional context | 额外信息
两个方案对比
方案1
同 react-router、remix 中的用法
优势是
声明式的,可以填空式的去补齐 error 状态、loading 状态下的 ui 展现,避免遗漏处理某些状态
不再 Suspense 内的组件,首次可以正常渲染
可以复用 react-router 的现有能力
方案2
同 react-query、 swc 中的写法
优势是
代码精简
如果 Route 中有多处同时依赖数据,只需要各自判断数据是否存在,不用都包裹 Suspense
其他需要考虑的点是:
- SSR 需要对齐用法,如果用了 useAsyncData,SSR 下返回的数据格式需要保持一致
- 加上非阻塞的模式和流式的模式,数据请求的方式一共有 3 种了,是否概念太多,如果是 方案一,是否需要将流式的用法统一过去
和 react use 的区别
使用了 react use 的组件,数据在加载过程中,整个组件是不渲染的,而我们需要的组件内不依赖数据的部分先行渲染
https://github.com/acdlite/rfcs/blob/first-class-promises/text/0000-first-class-support-for-promises.md#resuming-a-suspended-component-by-replaying-its-execution
async function fetchTodo(id) {
const data = await fetchDataFromCache(`/api/todos/${id}`);
return {contents: data.contents};
}
function Todo({id, isSelected}) {
const todo = use(fetchTodo(id));
return (
<div className={isSelected ? 'selected-todo' : 'normal-todo'}>
{todo.contents}
</div>
);
}
3条答案
按热度按时间vecaoik11#
useAyncData() 方法是必须的吗, 消费时直接复用 useData() 方法可以吗?
async 这个行为是在渲染之前就决定了 (defer), 渲染一直是同步函数, hooks 的习惯就是多次渲染, 所以理论上不需要再有一个 useAyncData 了
uqzxnwby2#
另外 defer 是推迟, 这个语义上貌似有问题
like unblocking? or streaming? any words else?
ifsvaxew3#
另外 defer 是推迟, 这个语义上貌似有问题
like unblocking? or streaming? any words else?
这里 defer 参照的是 react-router 的配置,猜想这里的 defer 应该是跟 script 标签中的 defer 含义类似,先触发加载,但不阻塞渲染