一. 简单的容错组件
封装
import React from 'react';
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
// 更新状态,以便下次渲染可以显示后备的UI
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
// 你同样可以将错误信息发送到一个错误报告服务
logErrorToMyService(error, errorInfo);
}
render() {
if (this.state.hasError) {
// 你可以渲染任何自定义的后备UI
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
使用
<ErrorBoundary>
<MyWidget />
</ErrorBoundary>
二. 使用 react-error-boundary
下载
npm install react-error-boundary
使用
import React from 'react';
import ErrorBoundary from 'react-error-boundary';
const MyApp = () => (
<ErrorBoundary
FallbackComponent={({ error, resetErrorBoundary }) => (
<div>
There was an error: <pre>{error.message}</pre>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
)}
onReset={() => {
// 处理重置逻辑
}}
>
<Suspense fallback={<div>Loading...</div>}>
<MyBrokenComponent />
</Suspense>
</ErrorBoundary>
);
三. 分析 react-error-boundary
捕获错误的 Api
getDerivedStateFromError:返回值会作为组件的 state 用于展示错误时的内容。
componentDidCatch:错误生命周期函数。
创建错误边界组件 Provider
错误边界组件其实是一个通过 Context.Provider 包裹的组件,这样使得组件内部可以获取到捕捉的相关操作
import { createContext } from "react";
export type ErrorBoundaryContextType = {
didCatch: boolean;
error: any;
resetErrorBoundary: (...args: any[]) => void;
};
// 错误边界组件其实是一个通过 Context.Provider 包裹的组件
export const ErrorBoundaryContext =
createContext<ErrorBoundaryContextType | null>(null);
定义错误边界组件
定义边界组件状态
type ErrorBoundaryState =
| {
didCatch: true;
error: any;
}
| {
didCatch: false;
error: null;
};
const initialState: ErrorBoundaryState = {
didCatch: false, // 错误是否捕捉
error: null, // 捕捉到的错误信息
};
捕捉错误
getDerivedStateFromError 捕捉到错误后,设置组件状态展示备份组件 componentDidCatch 用于触发错误回调
export class ErrorBoundary extends Component<
ErrorBoundaryProps,
ErrorBoundaryState
> {
constructor(props: ErrorBoundaryProps) {
super(props);
this.resetErrorBoundary = this.resetErrorBoundary.bind(this);
this.state = initialState;
}
static getDerivedStateFromError(error: Error) {
return { didCatch: true, error };
}
componentDidCatch(error: Error, info: ErrorInfo) {
this.props.onError?.(error, info);
}
}
渲染备份组件
通过指定的参数名区分是无状态组件还是有状态组件
- 无状态组件通过直接调用函数传递 props
- 有状态组件通过 createElement 传递 props 通过 createElement 处理传递的组件更加优雅
- createElement(元素类型,参数,子元素)详情,其中第一个参数可以直接传递 Context.Provider
export class ErrorBoundary extends Component<
ErrorBoundaryProps,
ErrorBoundaryState
> {
// ...
render() {
const { children, fallbackRender, FallbackComponent, fallback } =
this.props;
const { didCatch, error } = this.state;
let childToRender = children;
// 如果捕捉到了错误
if (didCatch) {
const props: FallbackProps = {
error,
resetErrorBoundary: this.resetErrorBoundary,
};
// 通过指定的参数名区分是无状态组件还是有状态组件
if (typeof fallbackRender === "function") {
childToRender = fallbackRender(props);
} else if (FallbackComponent) {
childToRender = createElement(FallbackComponent, props);
} else if (fallback === null || isValidElement(fallback)) {
childToRender = fallback;
} else {
if (isDevelopment) {
console.error(
"react-error-boundary requires either a fallback, fallbackRender, or FallbackComponent prop"
);
}
throw error;
}
}
// Context.Provider 可以直接作为 createElement 的第一个参数
return createElement(
ErrorBoundaryContext.Provider,
{
value: { // Context.Provider 提供可供消费的内容
didCatch,
error,
resetErrorBoundary: this.resetErrorBoundary,
},
},
childToRender
);
}
// ...
}
重置组件
将错误信息重置使得能渲染原组件
export class ErrorBoundary extends Component<
ErrorBoundaryProps,
ErrorBoundaryState
> {
// ...
render() {
const { children, fallbackRender, FallbackComponent, fallback } =
this.props;
const { didCatch, error } = this.state;
let childToRender = children;
// 如果捕捉到了错误
if (didCatch) {
const props: FallbackProps = {
error,
resetErrorBoundary: this.resetErrorBoundary,
};
// 通过指定的参数名区分是无状态组件还是有状态组件
if (typeof fallbackRender === "function") {
childToRender = fallbackRender(props);
} else if (FallbackComponent) {
childToRender = createElement(FallbackComponent, props);
} else if (fallback === null || isValidElement(fallback)) {
childToRender = fallback;
} else {
if (isDevelopment) {
console.error(
"react-error-boundary requires either a fallback, fallbackRender, or FallbackComponent prop"
);
}
throw error;
}
}
// Context.Provider 可以直接作为 createElement 的第一个参数
return createElement(
ErrorBoundaryContext.Provider,
{
value: { // Context.Provider 提供可供消费的内容
didCatch,
error,
resetErrorBoundary: this.resetErrorBoundary,
},
},
childToRender
);
}
// ...
}
通过 useHook 控制边界组件
- 通过 context 获取最近的边界组件内容
- 通过手动抛出错误重新触发边界组件
import { useContext, useMemo, useState } from "react";
import { assertErrorBoundaryContext } from "./assertErrorBoundaryContext";
import { ErrorBoundaryContext } from "./ErrorBoundaryContext";
type UseErrorBoundaryState<TError> =
| { error: TError; hasError: true }
| { error: null; hasError: false };
export type UseErrorBoundaryApi<TError> = {
resetBoundary: () => void;
showBoundary: (error: TError) => void;
};
export function useErrorBoundary<TError = any>(): UseErrorBoundaryApi<TError> {
// 获取最近的边界组件 Provider 的内容
const context = useContext(ErrorBoundaryContext);
// 断言 Context 是否为空
assertErrorBoundaryContext(context);
const [state, setState] = useState<UseErrorBoundaryState<TError>>({
error: null,
hasError: false,
});
const memoized = useMemo(
() => ({
resetBoundary: () => {
// 提供 Provider 对应的重置边界组件方法,渲染原组件
context.resetErrorBoundary();
setState({ error: null, hasError: false });
},
// 手动抛出错误,触发边界组件
showBoundary: (error: TError) =>
setState({
error,
hasError: true,
}),
}),
[context.resetErrorBoundary]
);
// 当调用 showBoundary 后,该 hook 会手动抛出错误,让边界组件来捕捉
if (state.hasError) {
throw state.error;
}
return memoized;
}
在 ErrorBoundary 中捕获异步错误
- 错误边界只捕获在 React 渲染周期中发生的错误。在它之外发生的事情,如 reject 的 Promise、带有 setTimeout的异步代码、各种回调和事件处理程序抛出的错误无法捕捉
- Dan Abramov 与我们分享了一个很酷的黑科技,正是为了实现这一点。先用 try/catch 捕捉这些错误,然后在 catch 语句中触发正常的 React 重渲染,然后把这些错误重新抛回重渲染周期
const Component = () => {
// create some random state that we'll use to throw errors
const [state, setState] = useState();
const onClick = () => {
try {
// something bad happened
} catch (e) {
// trigger state update, with updater function as an argument
setState(() => {
// re-throw this error within the updater function
// it will be triggered during state update
throw e;
})
}
}
}