症状
错误直接点名缺失件:“A component suspended while rendering, but no fallback UI was specified”——加上边界即可。更糟的是无报错的沉默变体:唯一边界包住整个应用,懒加载设置面板时整页消失在一个 spinner 后面。要画的地图是:哪些组件会挂起(lazy 导入、数据 hook)与边界相对它们的位置——边界远离挂起子组件等于全页降级,紧贴挂起单元等于局部 spinner、外壳完好。水合期挂起子组件没有边界时,整棵树被迫退回客户端渲染,表现为页面先服务端闪现再整树重挂。
诊断思路
先读错误区分两种变体:渲染期缺边界补边界,同步输入期挂起改用 transition;再绘制边界与 lazy/suspending 组件的相对位置图;最后给更新按紧急度分类——触发挂起的输入驱动更新必须是 transition,紧急渲染按设计不能等降级 UI。
根因
组件挂起时 React 向上找最近的 Suspense 边界并渲染其降级 UI。没有边界、或边界放得太高,要么抛出 missing-fallback 错误,要么整个子树藏进一个 spinner——片段加载让整页空白。
修复与验证
边界紧贴每个挂起单元:Timeline 与 Recommendations 各自的 Skeleton,Header 与 Shell 在一切边界之外始终渲染;触发数据加载的输入用 useTransition 的 start 包裹 setTab,旧标签页保持可交互直到新内容提交;降级 UI 设计要能撑住几何——匹配加载后布局的骨架屏让“加载”本身不再制造布局偏移与滚动重置。验证时懒加载片段期间外壳与兄弟区域保持渲染;触发数据加载的输入保持响应、旧内容驻留到新内容提交。
代码示例
Error: A component suspended while rendering, but no fallback UI
was specified. Add a <Suspense boundary=...> around it.
function ProfilePage() {
return (
<Shell> {/* outside any boundary: always rendered */}
<Header />
<Suspense fallback={<TimelineSkeleton />}>
<Timeline /> {/* suspends on data */}
</Suspense>
<Suspense fallback={<RecoSkeleton />}>
<Recommendations /> {/* suspends independently */}
</Suspense>
</Shell>
);
}
const [isPending, start] = useTransition();
function onSelect(tab) {
start(() => {
setTab(tab); // suspending data fetch marked non-urgent
});
}
// old tab stays interactive and visible until the new one commits