315 - 《Suspense》
1、为啥会有 Suspense?
其实最初是为了解 Client 的 CLS 问题。CLS 全称 Cumulative Layout Shift(累积布局偏移),可以看 web.dev 的详细介绍 ,如果没时间,通过下面两个形象的例子应该也能感受到是啥问题。
例子 1 来源于 Facebook Ad Manager,大家可以数数有多少个 Loading/Spinner 占位符,这应该是 Ajax 出来后比较典型的问题之一,也称「Spinner Hell」;例子 2 可太有趣了,哈哈,坑用户没商量。
Suspense 是 React 针对这个问题的解。比如下面代码的 Foo 和 Bar,如果都有 loading 状态,会统一先渲染 LoadingComponent,等 Foo 和 Bar 都渲染完成之后,再一下子显示出来。当然,在 Suspense 之外也自行通过维护大量的 Loading 状态来处理,只不过比较麻烦。
<Suspense fallback={<Spinner />}>
<Foo />
<Bar />
</Suspense>
大家可能会好奇的是,Suspense 是如何知道子组件里的 Foo 和 Bar 的加载状态的?
2、Suspense 原理。
Suspense 原理类似 Error Boundary,在通过 throw + try…catch 实现子组件消息的上报,这用于 Error Boundary 还算合理,用于 Suspense 总感觉有些奇怪。不同的是,用于 Suspense 时,throw 的是 Promise,然后 Suspense 基于 Promise 的状态觉得子组件的加载状态。
function Foo() {
const [fetchRequest] = React.useState(() => {
return fetch('/api/foo');
});
if (fetchRequest.status === 'pending') {
throw fetchRequest;
}
return </>;
}
React Query 和 SWR 都可通过配置切换到 Suspense 模式。
import useSWR from 'swr';
function Profile () {
const { data } = useSWR('/api/user', fetcher, { suspense: true })
return <div>hello, {data.name}</div>
}
3、Suspense 应用场景。
react.dev 官网 提供了大量 Suspense 的应用场景。
举一些场景的:
1)等一组组件全部加载完成后,一次性渲染
2)加载新内容时显示老内容(基于 useDeferredValue)
function Foo() {
const deferredQuery = useDeferredValue(query);
const isStale = query !== deferredQuery;
}
3)路由切换时等新页面的组件加载完成后再