React 进阶学习
本文记录了在React开发过程中遇到的一些进阶知识点和实践经验,涵盖错误边界、性能优化、状态管理等多个方面,持续更新中...
🛡️ ErrorBoundary 错误边界
什么是 ErrorBoundary?
在现代 React 应用开发中,错误处理是一个不可忽视的重要环节。一个未处理的 JavaScript 错误可能导致整个应用崩溃,给用户带来极差的体验。React 16 引入的 ErrorBoundary(错误边界) 机制,为我们提供了一种优雅的错误处理方案。
ErrorBoundary 是 React 组件中的一种特殊组件,它可以捕获其子组件树中任何地方的 JavaScript 错误,并记录这些错误,同时显示降级的 UI 而不是让整个组件树崩溃。
简单来说,ErrorBoundary 就像是 JavaScript 的 try-catch 语句,但它专门用于 React 组件。
ErrorBoundary 的核心规则
React 的 ErrorBoundary 有一些严格的"硬规则",这些规则决定了大部分实现都采用相似的固定写法:
1. 🔧 必须使用 Class 组件
ErrorBoundary 必须是 class 组件,因为函数组件无法实现错误捕获所需的生命周期方法:
componentDidCatchgetDerivedStateFromError
2. ⚙️ 必须实现特定的生命周期方法
至少要实现以下两个生命周期方法之一:
| 方法 | 作用 | 调用时机 |
|---|---|---|
static getDerivedStateFromError(error) | 更新 state,触发 fallback UI | 错误发生时,用于渲染降级 UI |
componentDidCatch(error, errorInfo) | 记录错误信息,上报监控 | render() 之后执行,适合日志记录 |
3. 🔄 错误处理流程
ErrorBoundary 的工作流程如下:
graph TD
A[组件渲染] --> B{是否发生错误?}
B -->|否| C[正常渲染 children]
B -->|是| D[React 捕获错误]
D --> E[调用 getDerivedStateFromError]
E --> F[更新 state.hasError = true]
F --> G[重新调用 render()]
G --> H[渲染 fallback UI]
H --> I[调用 componentDidCatch]
I --> J[记录错误日志]正常流程:
- React 调用
render()→this.state.hasError = false→ 返回this.props.children
错误流程:
- React 捕获错误,找到最近的
ErrorBoundary - 调用
static getDerivedStateFromError(error),更新state - React 重新调用
render()→ 返回this.props.fallback(错误 UI) - 调用
componentDidCatch,执行错误记录和上报
💻 完整的 ErrorBoundary 实现
基于上述核心规则,几乎所有 ErrorBoundary 的实现都遵循相同的模式。以下是一个功能完整、生产就绪的 ErrorBoundary 组件:
/**
* 🛡️ ErrorBoundary 错误边界组件
*
* 功能特性:
* - 捕获子组件中的 JavaScript 错误,防止整个应用崩溃
* - 提供友好的错误提示和恢复机制
* - 支持自定义 fallback UI 和错误处理回调
* - 开发环境下显示详细错误信息,生产环境下上报错误
*/
import { Component, ReactNode } from 'react'
interface Props {
children: ReactNode
fallback?: ReactNode | ((error: Error, reset: () => void) => ReactNode)
}
interface State {
hasError: boolean
error?: Error
}
export default class ErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props)
this.state = { hasError: false }
}
/**
* 📋 静态方法:从错误中派生状态
* 当子组件抛出错误时,React 会调用此方法
* 返回新的 state 来触发错误 UI 的渲染
*/
static getDerivedStateFromError(error: Error): State {
// 更新状态,下次渲染将显示错误 UI
return { hasError: true, error }
}
/**
* 🔍 生命周期方法:捕获并处理错误
* 在错误边界捕获到错误后调用,适合进行错误记录和上报
*/
componentDidCatch(error: Error, errorInfo: any) {
// 控制台记录错误详情
console.error('🚨 ErrorBoundary caught an error:', error, errorInfo)
// 生产环境下,将错误信息发送到监控服务
if (import.meta.env.PROD) {
this.reportErrorToService(error, errorInfo)
}
}
/**
* 📊 错误上报方法
* 将错误信息发送到监控服务(如 Sentry、LogRocket 等)
*/
private reportErrorToService(error: Error, errorInfo: any) {
// 示例:发送到错误监控服务
// Sentry.captureException(error, { contexts: { react: errorInfo } })
// 或者发送到自定义的错误收集 API
fetch('/api/error-report', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
message: error.message,
stack: error.stack,
componentStack: errorInfo.componentStack,
timestamp: new Date().toISOString(),
userAgent: navigator.userAgent,
url: window.location.href
})
}).catch(reportError => {
console.error('Failed to report error:', reportError)
})
}
/**
* 🔄 重试处理方法
* 重置错误状态,尝试重新渲染组件
*/
private handleRetry = () => {
this.setState({ hasError: false, error: undefined })
}
render() {
// 如果发生错误,渲染错误 UI
if (this.state.hasError) {
// 支持函数形式的 fallback,可以接收错误信息和重试方法
if (typeof this.props.fallback === 'function') {
return this.props.fallback(this.state.error!, this.handleRetry)
}
// 支持自定义的 fallback 组件
if (this.props.fallback) {
return this.props.fallback
}
// 默认的错误 UI - 美观且用户友好
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="max-w-md w-full bg-white shadow-lg rounded-lg p-6">
{/* 错误图标 */}
<div className="flex items-center justify-center w-12 h-12 mx-auto bg-red-100 rounded-full mb-4">
<svg
className="w-6 h-6 text-red-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"
/>
</svg>
</div>
<div className="text-center">
<h1 className="text-lg font-semibold text-gray-900 mb-2">
🚫 出现了一些问题
</h1>
<p className="text-sm text-gray-600 mb-6">
应用遇到了意外错误,请尝试刷新页面或联系技术支持。
</p>
{/* 开发环境下显示错误详情 */}
{import.meta.env.DEV && this.state.error && (
<details className="text-left mb-4 p-3 bg-gray-100 rounded text-xs">
<summary className="cursor-pointer font-medium text-gray-700">
🔍 错误详情 (开发模式)
</summary>
<pre className="mt-2 text-red-600 whitespace-pre-wrap">
{this.state.error.message}
{'\n'}
{this.state.error.stack}
</pre>
</details>
)}
{/* 操作按钮 */}
<div className="flex gap-3 justify-center">
<button
onClick={this.handleRetry}
className="px-4 py-2 bg-blue-600 text-white text-sm font-medium rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors"
>
🔄 重试
</button>
<button
onClick={() => window.location.reload()}
className="px-4 py-2 bg-gray-600 text-white text-sm font-medium rounded-md hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 transition-colors"
>
🔃 刷新页面
</button>
</div>
</div>
</div>
</div>
)
}
// 正常情况下,渲染子组件
return this.props.children
}
}🏗️ 实际应用场景
1. 全局错误边界
通常在应用的最外层包裹一个 ErrorBoundary,作为最后的安全网:
// App.tsx
function App() {
return (
<ErrorBoundary>
<Router>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/dashboard" element={<Dashboard />} />
</Routes>
</Router>
</ErrorBoundary>
)
}2. 局部错误边界
对于重要或容易出错的组件,可以单独包裹 ErrorBoundary:
// Dashboard.tsx
function Dashboard() {
return (
<div>
<Header />
<ErrorBoundary fallback={<ChartErrorFallback />}>
<ComplexChart />
</ErrorBoundary>
<ErrorBoundary fallback={<TableErrorFallback />}>
<DataTable />
</ErrorBoundary>
</div>
)
}⚠️ ErrorBoundary 的能力边界
了解 ErrorBoundary 能捕获和不能捕获的错误类型,对于正确使用它至关重要:
✅ 能够捕获的错误
| 错误类型 | 描述 | 示例 |
|---|---|---|
| 渲染期间的错误 | 组件 render 方法中的 JavaScript 错误 | undefined.property |
| 生命周期方法错误 | 类组件生命周期方法中的错误 | componentDidMount 中的异常 |
| 构造函数错误 | 整个组件树构造函数中的错误 | constructor 中的初始化错误 |
❌ 无法捕获的错误
| 错误类型 | 描述 | 解决方案 |
|---|---|---|
| 事件处理器错误 | 点击、输入等事件回调中的错误 | 使用 try-catch |
| 异步代码错误 | setTimeout、Promise、async/await | 使用 try-catch 或 .catch() |
| 服务端渲染错误 | SSR 过程中的错误 | 服务端错误处理机制 |
| ErrorBoundary 自身错误 | 错误边界组件内部的错误 | 上级 ErrorBoundary |
🛠️ 异步错误处理示例
// ❌ ErrorBoundary 无法捕获
const handleClick = async () => {
const data = await fetchData() // 如果这里出错,ErrorBoundary 捕获不到
}
// ✅ 正确的异步错误处理
const handleClick = async () => {
try {
const data = await fetchData()
setData(data)
} catch (error) {
console.error('Fetch failed:', error)
setError(error.message)
}
}⏳ Suspense 异步组件处理
核心概念
Suspense 是 React 16.6 引入的组件,用于处理异步操作的加载状态。它让我们可以声明式地处理组件的"等待"状态,而不需要手动管理 loading 状态。
常见用法
1. use hook (React 18+)
import { use, Suspense } from 'react'
// 创建 Promise
const userPromise = fetch('/api/user').then(res => res.json())
function UserProfile() {
// use hook 直接消费 Promise
const user = use(userPromise)
return <div>Hello, {user.name}</div>
}
// 使用时必须包裹 Suspense
function UserPage() {
return (
<Suspense fallback={<div>加载用户信息...</div>}>
<UserProfile />
</Suspense>
)
}2. 动态 Promise 处理
function UserProfile({ userId }) {
// 根据 props 动态创建 Promise
const userPromise = useMemo(() =>
fetch(`/api/users/${userId}`).then(res => res.json()),
[userId]
)
const user = use(userPromise)
return <div>Hello, {user.name}</div>
}3. 代码分割 (最常见)
import { lazy, Suspense } from 'react'
const Dashboard = lazy(() => import('../pages/Dashboard'))
function App() {
return (
<Routes>
<Route path="/dashboard" element={
<Suspense fallback={<PageSkeleton />}>
<Dashboard />
</Suspense>
} />
</Routes>
)
}🔄 与传统方式对比
❌ 传统的 Loading 处理
function UserProfile({ userId }) {
const [user, setUser] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
fetchUser(userId)
.then(setUser)
.catch(setError)
.finally(() => setLoading(false))
}, [userId])
if (loading) return <div>Loading...</div>
if (error) return <div>Error: {error.message}</div>
return <div>Hello, {user.name}</div>
}✅ Suspense + 数据获取
// 使用 Suspense 配合数据获取库(如 SWR、React Query)
function UserProfile({ userId }) {
const { data: user } = useSWR(`/api/users/${userId}`, fetcher, {
suspense: true // 启用 Suspense 模式
})
return <div>Hello, {user.name}</div> // 无需处理 loading 状态
}
function App() {
return (
<ErrorBoundary fallback={<ErrorUI />}>
<Suspense fallback={<UserSkeleton />}>
<UserProfile userId="123" />
</Suspense>
</ErrorBoundary>
)
}🎯 核心优势
| 对比维度 | 传统方式 | Suspense 方式 |
|---|---|---|
| 状态管理 | 手动管理 loading/error 状态 | 自动处理,组件专注业务逻辑 |
| 代码复杂度 | 每个组件都需要重复的状态逻辑 | 声明式,逻辑集中 |
| 用户体验 | 各组件独立 loading,体验割裂 | 统一的 loading 边界,体验连贯 |
| 错误处理 | 需要在每个组件处理错误 | 配合 ErrorBoundary 统一处理 |
🚀 实际项目应用场景
1. 列表页面的数据加载
// ❌ 传统方式 - 每个组件管理自己的 loading
function UserList() {
const [users, setUsers] = useState([])
const [loading, setLoading] = useState(true)
useEffect(() => {
fetchUsers().then(data => {
setUsers(data)
setLoading(false)
})
}, [])
if (loading) return <div>加载中...</div>
return users.map(user => <UserCard key={user.id} user={user} />)
}
function UserCard({ user }) {
const [profile, setProfile] = useState(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
fetchUserProfile(user.id).then(data => {
setProfile(data)
setLoading(false)
})
}, [user.id])
if (loading) return <div>加载用户详情...</div>
return <div>{profile.name} - {profile.role}</div>
}// ✅ Suspense 方式 - 声明式处理
function UserList() {
const usersPromise = useMemo(() => fetchUsers(), [])
const users = use(usersPromise)
return users.map(user => (
<Suspense key={user.id} fallback={<UserCardSkeleton />}>
<UserCard user={user} />
</Suspense>
))
}
function UserCard({ user }) {
const profilePromise = useMemo(() => fetchUserProfile(user.id), [user.id])
const profile = use(profilePromise)
return <div>{profile.name} - {profile.role}</div>
}
// 页面组件
function UsersPage() {
return (
<div>
<h1>用户列表</h1>
<Suspense fallback={<ListSkeleton />}>
<UserList />
</Suspense>
</div>
)
}2. 模态框中的异步内容
// ❌ 传统方式
function UserModal({ userId, open }) {
const [user, setUser] = useState(null)
const [loading, setLoading] = useState(false)
useEffect(() => {
if (open && userId) {
setLoading(true)
fetchUser(userId).then(data => {
setUser(data)
setLoading(false)
})
}
}, [open, userId])
return (
<Modal open={open}>
{loading ? <Spinner /> : user && <UserDetails user={user} />}
</Modal>
)
}// ✅ Suspense 方式
function UserModal({ userId, open }) {
return (
<Modal open={open}>
{userId && (
<Suspense fallback={<ModalSkeleton />}>
<UserDetails userId={userId} />
</Suspense>
)}
</Modal>
)
}
function UserDetails({ userId }) {
const userPromise = useMemo(() => fetchUser(userId), [userId])
const user = use(userPromise)
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
<p>{user.department}</p>
</div>
)
}3. Tab 切换的懒加载
// 实际项目中的 Tab 组件
function ProjectTabs({ projectId }) {
const [activeTab, setActiveTab] = useState('overview')
const tabs = [
{ id: 'overview', label: '概览', component: ProjectOverview },
{ id: 'members', label: '成员', component: ProjectMembers },
{ id: 'settings', label: '设置', component: ProjectSettings }
]
return (
<div>
<div className="tab-header">
{tabs.map(tab => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={activeTab === tab.id ? 'active' : ''}
>
{tab.label}
</button>
))}
</div>
<div className="tab-content">
<Suspense fallback={<TabContentSkeleton />}>
{tabs.find(tab => tab.id === activeTab)?.component &&
React.createElement(
tabs.find(tab => tab.id === activeTab).component,
{ projectId }
)
}
</Suspense>
</div>
</div>
)
}
// 各个 Tab 组件只关注数据展示
function ProjectMembers({ projectId }) {
const membersPromise = useMemo(() =>
fetchProjectMembers(projectId), [projectId]
)
const members = use(membersPromise)
return (
<div>
{members.map(member => (
<MemberCard key={member.id} member={member} />
))}
</div>
)
}⚡ 关键注意事项
1. use hook 的限制
// ❌ 错误用法 - 不能在条件语句中使用
function UserProfile({ shouldLoad }) {
if (shouldLoad) {
const user = use(userPromise) // 违反 Hook 规则
}
return null
}
// ✅ 正确用法 - 条件渲染整个组件
function UserPage({ shouldLoad }) {
return (
<div>
{shouldLoad && (
<Suspense fallback={<UserSkeleton />}>
<UserProfile />
</Suspense>
)}
</div>
)
}2. Promise 缓存问题
// ❌ 每次渲染都创建新 Promise
function UserProfile({ userId }) {
const user = use(fetch(`/api/users/${userId}`)) // 会重复请求
return <div>{user.name}</div>
}
// ✅ 使用 useMemo 缓存 Promise
function UserProfile({ userId }) {
const userPromise = useMemo(() =>
fetch(`/api/users/${userId}`).then(res => res.json()),
[userId]
)
const user = use(userPromise)
return <div>{user.name}</div>
}