Skip to content

错误边界:隔离范围、记录与恢复 ​

普通表单校验失败是预期业务状态,组件render抛出异常则意味着这一段UI无法继续描述。边界的任务是让这棵子树退出,显示可操作的替代界面,同时保留其他区域。它不是一个把所有try/catch都包起来的全局开关,也不会自动修好导致异常的数据。

用可修复的数据故障观察恢复 ​

完整替换 src/App.jsx。错误由卡片render读取到broken而产生;按钮只是修改数据,使下一次render抛错。边界外的草稿用于检验隔离范围。

jsx
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驱动两张卡。

查看答案。随后用渐进迁移决定哪些类应保留、哪些职责可以移动。

用心记录每一步成长