React:在应用程序中统一处理错误(react-error-boundary)
引言
在前一篇文章中,我们探讨了如何使用react自带的错误边界机制Error Boundary进行错误处理。Error Boundary是React中用于捕获渲染错误的机制,但它有一些局限性,例如无法处理事件处理程序中的错误或异步操作中的错误。为了解决这些问题,我们将介绍一个名为react-error-boundary的库,它提供了一个更为灵活的错误处理方案,能够轻松捕获组件中的错误、事件处理程序中的错误以及异步操作中的错误。
环境
- 操作系统:Windows 10
- Vite版本:v2.7.2
- Node版本:v19.0.0
- React版本:v19.0.0
- TypeScript版本:v5
react-error-boundary
react-error-boundary是由Facebook的React核心团队成员Brian Vaughn开发的一个库,旨在简化错误处理。它能够处理组件渲染错误、事件处理程序中的错误以及异步操作中的错误,极大地提升了开发者的体验。
安装方法
可以通过以下命令安装react-error-boundary:
npm install react-error-boundary
创建一个没有错误的示例程序
首先,我们将创建一个简单的示例程序,确保其正常运行。后续在这个基础上做错误验证的捕获及备用UI展示的测试。
page.tsx
import App from "./App";
export default function Home() {
return <App />;
}
components/App.tsx
import Page1 from "@/components/Page1";
import Page2 from "@/components/Page2";
import Page3 from "@/components/Page3";
import React, { ErrorInfo } from "react";
export default function App() {
return (
<>
<Page1 />
<Page2 />
<Page3 />
</>
);
}
components/Page1.tsx
export default function Page1() {
return (
<div style={{ backgroundColor: "lightblue", paddingBottom: "20px" }}>
<h3>Page1</h3>
</div>
);
}
components/Page2.tsx
export default function Page2() {
return (
<div style={{ backgroundColor: "yellow", paddingBottom: "20px" }}>
<h3>Page2</h3>
</div>
);
}
components/Page3.tsx
import Page3Child from "./Page3Child";
export default function Page3() {
return (
<div style={{ backgroundColor: "grey", paddingBottom: "20px" }}>
<h3>Page3</h3>
<Page3Child />
</div>
);
}
components/Page3Child.tsx
export default function Page3Child() {
const title = "Page3Child";
return (
<div style={{ backgroundColor: "orange", paddingBottom: "20px" }}>
<h5>{title}</h5>
</div>
);
}
基本用法
首先,我们需要创建一个用于显示错误信息的组件。该组件将在接收错误信息后显示。
components/ErrorFallback.tsx
"use client";
import { FallbackProps } from "react-error-boundary";
export default function ErrorFallback({ error }: FallbackProps) {
return (
<div>
<h2>发生错误</h2>
<pre>{error.message}</pre>
</div>
);
}
接下来,我们将修改Page3Child组件的代码,使其故意引发组件渲染的错误。
components/Page3Child.tsx
import { JSX } from "react";
function ThrowError(): JSX.Element {
throw new Error("Error in Page3Child");
}
export default function Page3Child() {
const title = "Page3Child";
return (
<div style={{ backgroundColor: "orange", paddingBottom: "20px" }}>
<h5>{title}</h5>
<ThrowError />
</div>
);
}
现在,我们将在Page3组件中使用ErrorBoundary来包裹Page3Child组件,并指定FallbackComponent为我们刚刚创建的错误页面组件ErrorFallback.tsx。
components/Page3.tsx
import ErrorFallback from "./ErrorFallback";
import Page3Child from "./Page3Child";
import { ErrorBoundary } from "react-error-boundary";
export default function Page3() {
return (
<div style={{ backgroundColor: "grey", paddingBottom: "20px" }}>
<h3>Page3</h3>
<ErrorBoundary FallbackComponent={ErrorFallback}>
<Page3Child />
</ErrorBoundary>
</div>
);
}
此时,当Page3Child组件引发错误时,ErrorBoundary将捕获该错误并显示ErrorFallback中显示错误信息的组件。
修改ErrorBoundary的包裹范围
我们可以尝试修改ErrorBoundary的包裹范围。首先,我们将删除在Page3组件中使用的ErrorBoundary,并在App组件中包裹Page3组件。
components/App.tsx
"use client";
import Page1 from "@/components/Page1";
import Page2 from "@/components/Page2";
import Page3 from "@/components/Page3";
import ErrorBoundary from "@/components/ErrorBoundary";
import { Page4 } from "@/components/Page4";
import Page5 from "@/components/Page5";
export default function App() {
return (
<>
<Page1 />
<Page2 />
<ErrorBoundary>
<Page3 />
</ErrorBoundary>
</>
);
}
此时,Page3部分将被完全替换为ErrorFallback中显示错误信息的组件。
接下来,我们将Page2和Page3组件包裹在ErrorBoundary组件中。
components/App.tsx
import { ErrorBoundary } from 'react-error-boundary';
import ErrorFallback from './ErrorFallback';
import Page1 from './Page1';
import Page2 from './Page2';
import Page3 from './Page3';
function App() {
return (
<>
<Page1 />
<ErrorBoundary FallbackComponent={ErrorFallback}>
<Page2 />
<Page3 />
</ErrorBoundary>
</>
);
}
export default App;
此时,Page2和Page3的内容将被完全替换为ErrorFallback中显示错误信息的组件。
最后,我们将所有组件都包裹在ErrorBoundary组件中。
components/App.tsx
import Page1 from "@/components/Page1";
import Page2 from "@/components/Page2";
import Page3 from "@/components/Page3";
import { ErrorBoundary } from "react-error-boundary";
import ErrorFallback from "@/components/ErrorFallback";
export default function App() {
return (
<>
<ErrorBoundary FallbackComponent={ErrorFallback}>
<Page1 />
<Page2 />
<Page3 />
</ErrorBoundary>
</>
);
}
此时,整个页面将被完全替换为ErrorFallback中显示错误信息的组件。
使用resetErrorBoundary重新加载出现错误的组件
在发生错误时,我们可以在用于异常显示的ErrorFallback组件中添加一个重试按钮,当用户点击重置按钮时,调用resetErrorBoundary回调函数实现中的所有组件的重新加载。
components/ErrorFallback.tsx
"use client";
import { FallbackProps } from "react-error-boundary";
function ErrorFallback({ error, resetErrorBoundary }: FallbackProps) {
return (
<div>
<h2>发生错误</h2>
<pre>{error.message}</pre>
<button type="button" onClick={resetErrorBoundary}>
再试一次
</button>
</div>
);
}
export default ErrorFallback;
接下来,我们将Page3Child修改为高频率引发错误的状态,以便用户可以尝试重试失败和成功的效果。
components/Page3Child.tsx
"use client";
import React, { JSX } from "react";
import { ErrorBoundary } from "react-error-boundary";
import ErrorFallback from "./ErrorFallback";
function ThrowError(): JSX.Element {
console.log("Page3Child 开始抛出错误");
if (Math.random() < 0.5) {
console.log("Page3Child 引发错误");
throw new Error("Page3Child 引发错误");
}
return <p>未引发错误</p>;
}
function CustomeH5(): JSX.Element {
console.log("加载CustomeH5");
return <h5>Page3Child</h5>;
}
function Page3Child() {
return (
<ErrorBoundary FallbackComponent={ErrorFallback}>
<div style={{ backgroundColor: "#DEB331" }}>
<CustomeH5 />
<ThrowError />
</div>
</ErrorBoundary>
);
}
export default Page3Child;
点击再试一次的时候,包裹在ErrorBoundar里面的组件会重新加载。
捕获事件处理中的错误并重置
如果在点击事件中出现错误,可以被react-error-boundary捕获后跳转到错误页面,如果希望消除错误重新返回正常页面重试一次的情况下。 可以在捕获错误的页面中添加onReset方法,使用onReset可以指定重置错误状态的回调函数。 也可以指定resetKeys,通过监听resetKeys指定的数组中的数据,如果发生改变,也会重置错误更新画面。
components/Page3Child.tsx
"use client";
import React, { ErrorInfo } from "react";
import { ErrorBoundary, FallbackProps } from "react-error-boundary";
import ErrorFallback from "./ErrorFallback";
function ThrowError(): React.JSX.Element {
throw new Error("Page3Child 引发错误");
}
function onError(error: Error, info: ErrorInfo) {
console.log("error.message", error.message);
console.log("info.componentStack:", info.componentStack);
}
function Page3Child() {
const [throwError, setThrowError] = React.useState(false);
const onclick = () => {
setThrowError((preThrowError) => !preThrowError);
};
const onReset = () => {
setThrowError((prev) => !prev);
};
function CustomeH5(): React.JSX.Element {
console.log("加载CustomeH5");
return <h5>Page3Child</h5>;
}
return (
<ErrorBoundary
FallbackComponent={ErrorFallback}
onError={onError}
onReset={onReset}
>
<div style={{ backgroundColor: "#DEB331" }}>
<CustomeH5 />
<button onClick={onclick}>抛出异常</button>
{throwError && <ThrowError />}
</div>
</ErrorBoundary>
);
}
export default Page3Child;
上面的代码块中,通过onReset={onReset}重新计算throwError的数据控制异常组建的显示或隐藏,通过点击再试一次的按钮可以再次执行onclick事件里面的方法。
JS事件处理程序中的错误
接下来,我们将介绍如何处理JS事件处理程序中的错误。我们将创建一个新的Page4组件,点击按钮时引发异常。
components/Page4.tsx
"use client";
import { useErrorBoundary } from "react-error-boundary";
function Page4() {
const { showBoundary } = useErrorBoundary();
const onClick = () => {
try {
throw new Error("Page4 引发错误");
} catch (error: any) {
showBoundary(error);
}
};
return (
<div style={{ backgroundColor: "#FF7272" }}>
<h3>Page4</h3>
<button type="button" onClick={onClick}>
按钮
</button>
</div>
);
}
export default Page4;
在事件处理程序中引发的异常将向上传播,直到被ErrorBoundary捕获。在这里,我们将Page4组件包裹在App组件的ErrorBoundary中。
components/App.tsx
"use client";
import ErrorFallback from "@/components/ErrorFallback";
import Page1 from "@/components/Page1";
import Page2 from "@/components/Page2";
import Page3 from "@/components/Page3";
import Page4 from "@/components/Page4";
import React, { ErrorInfo } from "react";
import { ErrorBoundary } from "react-error-boundary";
const onError = (error: Error, info: ErrorInfo) => {
console.log("错误信息:", error.message);
console.log("组件栈:", info.componentStack);
};
export default function App() {
return (
<>
<Page1 />
<Page2 />
<ErrorBoundary FallbackComponent={ErrorFallback} onError={onError}>
<Page3 />
<Page4 />
</ErrorBoundary>
</>
);
}
处理异步操作中的错误
最后,我们将介绍如何处理异步操作中的错误。我们将创建一个新的Page5组件,点击按钮时进行异步操作以获取用户名。
components/Page5.tsx
import { useState } from "react";
import { useErrorBoundary } from "react-error-boundary";
const fetchUserAPI = async (): Promise<string> => {
const username = "piyoko";
const ss = new Date().getMilliseconds();
if (ss % 2 === 0) {
throw new Error("fetchUserAPI中的错误");
}
return username;
};
function Page5() {
const [userName, setUserName] = useState("");
const { showBoundary } = useErrorBoundary();
const onClick = () => {
fetchUserAPI()
.then((res) => {
setUserName(res);
})
.catch((err) => {
showBoundary(err);
});
};
return (
<div style={{ backgroundColor: "#8CCDB0" }}>
<h3>Page5</h3>
<button type="button" onClick={onClick}>
按钮
</button>
<p>用户名:{userName}</p>
</div>
);
}
export default Page5;
在异步操作中引发的错误将被ErrorBoundary捕获,直到被上层组件处理。
components/App.tsx
"use client";
import ErrorFallback from "@/components/ErrorFallback";
import Page1 from "@/components/Page1";
import Page2 from "@/components/Page2";
import Page3 from "@/components/Page3";
import Page4 from "@/components/Page4";
import Page5 from "@/components/Page5";
import React, { ErrorInfo } from "react";
import { ErrorBoundary } from "react-error-boundary";
const onError = (error: Error, info: ErrorInfo) => {
console.log("错误信息:", error.message);
console.log("组件栈:", info.componentStack);
};
export default function App() {
return (
<>
<Page1 />
<Page2 />
<ErrorBoundary FallbackComponent={ErrorFallback} onError={onError}>
<Page3 />
<Page4 />
<Page5 />
</ErrorBoundary>
</>
);
}
总结
通过使用react-error-boundary,我们能够轻松处理组件中的错误、事件处理程序中的错误以及异步操作中的错误。这种方法不仅提高了代码的可维护性,还增强了用户体验,使得错误处理变得更加优雅和高效。
希望这篇文章能帮助你更好地理解如何在React应用程序中实现全面的错误处理机制!