今天复习了 React 工程化中的六个核心问题:错误边界、组件库设计、原子设计、受保护路由、状态管理选型和状态提升。以下是问答整理与知识补充。
1. 什么是错误边界(Error Boundaries)?如何实现?
我的回答:
-
错误边界不能捕获事件处理器和异步代码里的错误,这些需要用
try/catch。只捕获渲染阶段的错误,进行UI降级。 -
类组件中需要两个生命周期方法:
getDerivedStateFromError(用于渲染降级 UI)和componentDidCatch(用于记录错误日志)。 -
错误边界可以放在根组件防止页面白屏,也可以包裹子组件,实现局部降级。
-
React 19 可以直接引入 <ErrorBoundary> 组件并用 fallback 属性指定兜底 UI;旧版 React 需要安装 react-error-boundary 库。
补充:
- 错误边界不能捕获的错误包括:
- 事件处理器内部的错误(需 try/catch)
- 异步代码(setTimeout、requestAnimationFrame、Promise)
- 服务端渲染(SSR)期间抛出的错误
- 错误边界自身抛出的错误(无法捕获自己)
- 错误边界能捕获的错误包括:
- 组件渲染时的错误(render 期间)
- 生命周期方法里的错误
- 子组件树抛出的错误
- 函数组件 return 里的错误
- Hooks 渲染阶段的错误
-
getDerivedStateFromError 是静态方法,接收错误参数,返回新状态(如 { hasError: true })。它会在渲染阶段调用,不允许有副作用。
-
componentDidCatch 在提交阶段调用,可执行日志上报等副作用。
javascriptclass ErrorBoundary extends React.Component { static getDerivedStateFromError(error) { // 返回新 state → 触发降级 UI return { hasError: true }; } componentDidCatch(error, info) { // 副作用:上报错误 console.error(error, info); } render() { if (this.state.hasError) { return <h1>出错了</h1>; } return this.props.children; } } -
推荐将错误边界放在组件树的顶层(包裹路由出口)以及关键子模块周围,实现细粒度降级。
扩展:
-
函数组件中可以用 react-error-boundary 提供的 useErrorHandler Hook 处理事件回调中的错误。
-
如何测试错误边界?可以使用 render 和 act 配合抛出错误。
javascriptimport { render, screen } from '@testing-library/react'; import { act } from 'react-dom/test-utils'; // 错误边界组件 class ErrorBoundary extends React.Component { constructor(props) { super(props); this.state = { hasError: false }; } static getDerivedStateFromError() { return { hasError: true }; } render() { if (this.state.hasError) { return <div>出错啦</div>; } return this.props.children; } } // 一渲染就报错的组件 const Crash = () => { throw new Error('test error'); }; // 测试用例 test('错误边界应该捕获渲染错误', () => { //让 React 的所有状态更新、渲染、副作用都同步执行完成 act(() => { render( <ErrorBoundary> <Crash /> </ErrorBoundary> ); }); //页面上能找到 "出错啦" 这句话,并且这个元素确实在 DOM 里。 expect(screen.getByText('出错啦')).toBeInTheDocument(); });
2. 如果让你设计一个组件库,你会考虑哪些方面?
我的回答:
- 从开发者和用户体验出发:完善的文档、TypeScript 支持、可自定义样式(通过 className 或主题变量)、易测试。
- 构建与发布:按需加载(如 babel-plugin-import)、支持多种模块格式(ESM、CJS)、版本管理。
- 组件筛选:优先包含通用性强、高频使用的组件(按钮、输入框、轮播图等)。
补充:
-
API 设计 :遵循一致性原则,如尺寸(
size="small")、变体(variant="primary")、回调命名(onChange、onClick)。 -
样式方案:
- CSS-in-JS(如 styled-components):适合动态样式,但运行时开销大。
- CSS Modules:静态样式,无运行时,支持按需加载。
- 纯 CSS + 主题变量(CSS Variables):适合多主题切换。
-
**可访问性(a11y):**支持键盘导航、aria 属性、语义化标签。
-
测试策略:单元测试(Jest + React Testing Tools)、视觉回归测试(Chromatic)、端到端测试(Cypress)。
-
**发布与版本:**遵循语义化版本(SemVer)。
扩展:
- 如何实现按需加载?可以使用 babel-plugin-import(编译时替换路径) 或直接导出 ESM 格式,利用 Tree Shaking(打包阶段删除未使用代码)。
3. 谈谈你对原子设计(Atomic Design)的理解
我的回答:
- 五个层级:原子、分子、组织、模板、页面。
- 原子:按钮、输入框。
- 分子:输入框 + 按钮(搜索框)。
- 组织:导航栏、侧边栏。
- 模板:页面架构(无真实数据)。
- 页面:填充数据后的完整页面。
- 优点:复用性强、便于维护、样式统一。
- 缺点:开发成本高,不适合小型项目,对新手不友好。
- 在网易云项目中,SongItem 作为可复用的分子/组织组件。
补充:
- 缺点还包括:过度拆分可能导致组件碎片化;组件层级过深,数据传递复杂。
扩展:
- 在大型项目中,如何避免原子设计带来的性能问题?(使用 React.memo、懒加载等)
4. 如何实现一个受保护的路由(Protected Route)?
我的回答:
- 核心逻辑:判断是否登录,未登录则重定向到登录页(<Navigate to="/login" />);已登录则渲染子路由(<Outlet />)或指定组件。
- 角色权限:根据用户角色动态生成路由或控制侧边栏显示。
- 保存原路径:将当前路径存入 state,登录后跳回原路径。
- 异步鉴权:先展示全局 loading,请求接口验证 token 有效后再决定跳转。
补充:
-
受保护路由通常用包裹组件 或布局组件 实现:
javascriptfunction ProtectedRoute({ children }) { const { user } = useAuth(); if (!user) return <Navigate to="/login" replace />; return children; } // 使用 <Route path="/dashboard" element={ <ProtectedRoute> <Dashboard /> </ProtectedRoute> } /> -
使用 replace 属性避免重定向后无法返回。
-
保存原路径:通过 useLocation 获取当前路径,作为 state 传给登录页;登录成功后从 location.state 中读取并跳转。
5. React 项目中,你是如何做状态管理选型的?
我的回答:
- 组件内部状态用 useState;小型、静态的跨组件通信用 Context,但要注意性能(配合 React.memo)。
- 大型项目使用 Redux Toolkit(RTK)。
- 选型判断标准:状态范围、更新频率、调试需求、团队习惯。
补充:
- 状态分类:
- 本地状态(组件内部):useState、useReducer
- 跨组件状态(少量、低频):Context + useReducer
- 全局状态(大量、高频、复杂逻辑):Redux / Zustand / MobX
- **Context 性能问题:**当 Context 值变化时,所有 useContext 的组件都会重渲染。解决方案:拆分 Context(将不变和变化的值分开),或使用 useMemo 稳定 value。
- **Redux Toolkit 优势:**内置 Immer(简化不可变更新)、Thunk(异步)、DevTools 集成。
- Zustand 轻量级,适合中小项目,无需 Provider 包裹。
扩展:
- 什么是"乐观更新"?如何在 RTK 中实现?
- 先假设接口一定会成功,直接更新 UI,等请求真正返回后再做最终确认;如果失败再回滚。
6. "状态提升"的优缺点是什么?它的边界在哪里?
我的回答:
- 定义:将多个子组件共同依赖的状态提升到最近的父组件中,由父组件统一管理并通过 props 传递。
- 优点:数据来源清晰(单一数据源),便于调试和维护。
- 缺点:可能导致 props drilling(逐层传递),造成不必要的重渲染,需要配合 React.memo 或 useMemo 优化。
- 边界:提升到最近的共同父组件即可,不必再向上;当状态需要被较远层级的组件使用时,应考虑全局状态管理(Context / Redux)。
补充:
- **优点:**状态逻辑集中,避免重复;符合 React 单向数据流;易于实现时间旅行调试
- **缺点:**中间组件可能被迫接收无关 props,增加耦合;重构时可能需要移动大量状态
今日知识点总结
| 问题 | 核心要点 |
| 错误边界 | 类组件 getDerivedStateFromError + componentDidCatch;不能捕获事件/异步错误;React 19 内置 ErrorBoundary |
| 组件库设计 | API 一致性、TypeScript、文档、按需加载、可访问性、测试、版本管理 |
| 原子设计 | 原子 → 分子 → 组织 → 模板 → 页面;优点复用性,缺点开发成本高 |
| 受保护路由 | 基于登录状态重定向;支持角色权限、保存原路径、异步鉴权 loading |
| 状态管理选型 | 本地 useState → 少量跨组件 Context → 复杂全局 RTK/Zustand;注意 Context 性能 |
| 状态提升 | 提升到最近共同父组件;优点单一数据源,缺点 props drilling;超出范围用全局状态 |
|---|