错误边界:隔离范围、记录与恢复
普通表单校验失败是预期业务状态,组件render抛出异常则意味着这一段UI无法继续描述。边界的任务是让这棵子树退出,显示可操作的替代界面,同时保留其他区域。它不是一个把所有try/catch都包起来的全局开关,也不会自动修好导致异常的数据。
用可修复的数据故障观察恢复
完整替换 src/App.jsx。错误由卡片render读取到broken而产生;按钮只是修改数据,使下一次render抛错。边界外的草稿用于检验隔离范围。
import { Component, useState } from 'react'
export class Boundary extends Component {
state = { error: null }
static getDerivedStateFromError(error) { return { error } }
componentDidCatch(error, info) {
this.props.onReport?.({ message: error.message, componentStack: info.componentStack })
}
componentDidUpdate(previousProps) {
if (previousProps.resetKey !== this.props.resetKey && this.state.error !== null) {
this.setState({ error: null })
}
}
render() {
if (this.state.error !== null) {
return <section role="alert">
<p>卡片暂时不可用</p>
<button onClick={this.props.onRepair}>修复数据并重试</button>
</section>
}
return this.props.children
}
}
function Card({ broken }) {
if (broken) throw new Error('示例卡片数据损坏')
return <p>卡片正文</p>
}
export default function App() {
const [broken, setBroken] = useState(false)
const [revision, setRevision] = useState(0)
const [draft, setDraft] = useState('')
function repair() {
setBroken(false)
setRevision(n => n + 1)
}
return <main>
<label>边界外草稿<input value={draft} onChange={e => setDraft(e.target.value)} /></label>
<button onClick={() => setBroken(true)}>破坏卡片</button>
<Boundary resetKey={revision} onRepair={repair} onReport={entry => console.info(entry.message)}>
<Card broken={broken} />
</Boundary>
</main>
}输入草稿后破坏卡片,卡片正文消失,边界显示错误区域,草稿仍在且可继续编辑。修复后正文恢复,草稿保持。开发控制台仍可能显示React诊断错误,这是故障实验的预期信号,不能以控制台安静判断隔离是否生效。
从抛错到fallback的两类工作
getDerivedStateFromError是静态渲染阶段方法,收到错误并返回用于下一次render的state片段。它应纯粹,只决定显示什么;componentDidCatch在错误处理提交阶段负责报告,可得到组件栈。不要把上报放进render或派生方法,否则渲染重试可能制造额外副作用。生产报告还应限字段和净化内容,异常message并不天然可公开。
边界捕获后代渲染及相关生命周期错误,但不会捕获自身fallback再次抛错。边界越大,失败时被替换的区域越大;把整页都包在一个边界里,局部卡片失败也会让整页草稿退出。选择粒度应依据用户能否继续完成其他任务,不是“包得越外越安全”。
事件处理器自身抛出的错误、普通setTimeout回调里的异常,以及服务器渲染错误不受这个客户端边界统一处理。事件中的预期失败应在事件流程用try/catch更新错误状态。第五章会说明lazy模块拒绝如何进入渲染错误路径;不能把普通fetch失败也自动当成Suspense或边界错误。
重置边界不等于修复故障
上例repair先把broken改回false,再改变resetKey。resetKey是我们定义的普通prop,React没有自动重试协议;didUpdate观察它变化,清空已捕获错误后重新尝试子树。若只增加revision但broken仍为true,下一次render立即再次抛错。若卡片拥有局部state,先前失败卸载后重试会新建它,不能承诺保留边界内未保存草稿。
也可用key重建整个边界以实现重置,但那会一起重置边界身份及其子树。对于失败lazy资源,缓存的Promise可能仍然拒绝,必须另行处理资源重试。无论采用哪一种方式,都要先决定错误原因是否已变化。
练习:第二张卡片独立失败
把App改成两个Boundary实例,分别管理甲、乙卡片的broken和revision。破坏甲时乙正文保持;修复甲后两张正文同时出现。每张边界的修复按钮应有不同可访问名称,边界外草稿在全程不丢失。不要用同一个broken驱动两张卡。