React 并发特性:Suspense 与 Error Boundary 协作的最佳实践
React 并发特性Suspense 与 Error Boundary 协作的最佳实践一、并发渲染带来的新问题React 18 引入并发特性后组件的渲染不再是同步的。startTransition、useDeferredValue等 API 让渲染可以被中断和恢复。这带来了两个关键问题第一组件在渲染过程中可能因为数据未就绪而暂停。Suspense 负责处理这种暂停状态展示降级 UI。第二渲染期间可能抛出错误。Error Boundary 负责捕获这些错误防止整个应用崩溃。过去Suspense 和 Error Boundary 各司其职。但在并发模式下它们的协作变得紧密。一个常见的场景异步数据加载中请求失败。此时 Suspense 还在等待数据而 Promise 已经 reject。需要两层边界协同工作。flowchart TB A[组件渲染开始] -- B{数据是否就绪} B --|否| C[Suspense 挂起] C -- D[显示 fallback UI] D -- E{数据加载结果} E --|成功| F[正常渲染] E --|失败| G[抛出错误] B --|是| F B --|渲染异常| G G -- H[Error Boundary 捕获] H -- I[显示错误 UI] F -- J[组件挂载完成] I -- K[提供重试入口]二、Error Boundary 的正确用法React 16 引入了 Error Boundary但至今仍有一些常见的误用。最关键的约束是Error Boundary 只能捕获渲染阶段、生命周期方法和构造函数中的错误。它无法捕获事件处理器、异步代码和服务端渲染中的错误。import { Component, ReactNode, ErrorInfo } from react; interface ErrorBoundaryProps { children: ReactNode; fallback: ReactNode; onError?: (error: Error, info: ErrorInfo) void; onReset?: () void; } interface ErrorBoundaryState { hasError: boolean; error: Error | null; } class ErrorBoundary extends ComponentErrorBoundaryProps, ErrorBoundaryState { constructor(props: ErrorBoundaryProps) { super(props); this.state { hasError: false, error: null }; } static getDerivedStateFromError(error: Error): ErrorBoundaryState { return { hasError: true, error }; } componentDidCatch(error: Error, info: ErrorInfo): void { this.props.onError?.(error, info); // 上报到监控系统 try { fetch(/api/error-report, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ message: error.message, stack: error.stack, componentStack: info.componentStack, timestamp: Date.now(), }), }).catch(() { // 上报失败不应中断用户操作 }); } catch { // 静默处理避免二次错误 } } handleReset () { this.props.onReset?.(); this.setState({ hasError: false, error: null }); }; render(): ReactNode { if (this.state.hasError) { if (typeof this.props.fallback function) { return (this.props.fallback as (props: { error: Error | null; reset: () void; }) ReactNode)({ error: this.state.error, reset: this.handleReset, }); } return this.props.fallback; } return this.props.children; } }关键设计点getDerivedStateFromError是同步的只做状态设置不能有副作用。componentDidCatch用于副作用日志上报。需包裹 try-catch 避免上报本身抛错。提供onReset回调让父组件在重置时重新获取数据。Function 组件中的使用方案Error Boundary 必须是 class 组件这在 Function 组件主导的 React 18 项目中显得格格不入。推荐方案是封装一个 HOC高阶组件将 Error Boundary 藏在使用细节背后import { Component, ReactNode } from react; interface WithErrorBoundaryOptions { fallback: (props: { error: Error | null; reset: () void }) ReactNode; onError?: (error: Error, info: ErrorInfo) void; } function withErrorBoundaryT( WrappedComponent: React.ComponentTypeT, options: WithErrorBoundaryOptions ) { return function WithErrorBoundaryWrapper(props: T) { return ( ErrorBoundary fallback{options.fallback} onError{options.onError} WrappedComponent {...props} / /ErrorBoundary ); }; } // 使用示例 const SafeProductList withErrorBoundary(ProductList, { fallback: ({ reset }) ( div rolealert p商品列表加载失败/p button onClick{reset}重试/button /div ), });这种封装方式让 Function 组件的使用体验保持一致同时享受 Error Boundary 的保护。实际项目中建议将withErrorBoundary放在组件导出的位置而非在页面级别包裹。这样能实现更细粒度的错误隔离。三、Suspense 与 Error Boundary 的嵌套策略两者的嵌套顺序至关重要。错误的嵌套会导致错误被漏掉或 fallback UI 不符合预期。推荐策略Error Boundary 在外层Suspense 在内层。flowchart LR subgraph 推荐结构 A1[ErrorBoundary] -- A2[Suspense] A2 -- A3[AsyncComponent] end subgraph 错误结构 B1[Suspense] -- B2[ErrorBoundary] B2 -- B3[AsyncComponent] end推荐结构的执行流程组件挂起Suspense 触发→ 显示 Loading UI。数据加载失败 → Promise reject 导致组件 throw → Error Boundary 捕获。Error Boundary 显示错误 UI 并提供重试。错误结构的后果如果组件在 catch 之前就挂起了错误 UI 不会出现。用户停在 Loading 状态看起来像卡住了。实际组件使用示例function ProductPage({ productId }: { productId: string }) { return ( ErrorBoundary fallback{({ error, reset }) ( div rolealert h3页面加载失败/h3 p{error?.message ?? 未知错误}/p button onClick{reset}重试/button /div )} onError{(error, info) { console.error([ProductPage], error, info.componentStack); }} onReset{() { // 清除缓存后重试 queryClient.invalidateQueries({ queryKey: [product, productId] }); }} Suspense fallback{ProductSkeleton /} ProductDetail productId{productId} / /Suspense /ErrorBoundary ); }多个独立数据块的嵌套策略实际项目中一个页面可能包含多个独立的数据块如主内容、侧边栏推荐、评论区。推荐为每个独立数据块设置独立的 Suspense Error Boundary 组合function DashboardPage() { return ( div classNamedashboard ErrorBoundary fallback{div主内容加载失败/div} Suspense fallback{Skeleton /} MainContent / /Suspense /ErrorBoundary ErrorBoundary fallback{div推荐内容暂时无法显示/div} Suspense fallback{Skeleton /} SidebarRecommendations / /Suspense /ErrorBoundary ErrorBoundary fallback{div评论区加载失败/div} Suspense fallback{Skeleton /} Comments / /Suspense /ErrorBoundary /div ); }这种细粒度的错误边界设置确保单个数据块的失败不会影响整个页面的渲染。用户至少能看到其他正常加载的内容而不是看到一个空白的错误页面。这也是 React 官方推荐的实践模式。四、并发模式下的边界测试策略并发特性使错误场景的测试变得更复杂。需要覆盖以下场景场景SuspenseError Boundary预期结果数据正常加载短暂挂起不触发正常渲染数据加载超时挂起超时不触发显示超时 UI数据加载失败不触发触发显示错误 UI 重试渲染时抛出异常不触发触发显示错误 UI重试后成功短暂挂起重置正常渲染使用 React Testing Library 进行测试import { render, screen, waitFor } from testing-library/react; import { Suspense } from react; describe(ProductPage Error Handling, () { it(should show error boundary when data fetch fails, async () { // 模拟数据加载失败 const fetchProduct vi.fn().mockRejectedValue( new Error(网络请求失败) ); render( ErrorBoundary fallback{div加载失败/div} Suspense fallback{div加载中.../div} ProductDetail useData{fetchProduct} / /Suspense /ErrorBoundary ); // 先看到 loading expect(screen.getByText(加载中...)).toBeInTheDocument(); // 等待错误出现 await waitFor(() { expect(screen.getByText(加载失败)).toBeInTheDocument(); }, { timeout: 5000 }); }); it(should recover after retry, async () { let shouldFail true; const fetchProduct vi.fn().mockImplementation(() { if (shouldFail) { return Promise.reject(new Error(失败)); } return Promise.resolve({ id: 1, name: Test Product }); }); let resetCallback: (() void) | null null; render( ErrorBoundary fallback{({ reset }) { resetCallback reset; return button onClick{reset}重试/button; }} Suspense fallback{div加载中.../div} ProductDetail useData{fetchProduct} / /Suspense /ErrorBoundary ); await waitFor(() { expect(screen.getByText(重试)).toBeInTheDocument(); }); // 第二次调用成功 shouldFail false; resetCallback?.(); await waitFor(() { expect(screen.getByText(Test Product)).toBeInTheDocument(); }); }); });批量使用 Suspense Error Boundary 的模式还有一些性能注意事项。多个 Suspense 嵌套时React 会按嵌套层级依次渲染。建议将独立的数据加载拆分为独立的 Suspense 边界避免一个慢请求阻塞整个页面。五、总结Error Boundary 在外、Suspense 在内是并发模式下处理异步错误的黄金法则。这个嵌套顺序保证了错误能被正确捕获用户不会困在 Loading 状态。关键实践包括Error Boundary 的副作用包裹 try-catch、提供 onReset 回调实现状态重置、独立的数据加载使用独立的 Suspense 边界。测试层面需要覆盖加载中、失败、重试三个核心路径确保边界协作的健壮性。

相关新闻