-
-
Notifications
You must be signed in to change notification settings - Fork 198
/
ErrorBoundary.ts
128 lines (108 loc) · 3.1 KB
/
ErrorBoundary.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
import { isDevelopment } from "#is-development";
import { Component, createElement, ErrorInfo, isValidElement } from "react";
import { ErrorBoundaryContext } from "./ErrorBoundaryContext";
import { ErrorBoundaryProps, FallbackProps } from "./types";
type ErrorBoundaryState =
| {
didCatch: true;
error: any;
}
| {
didCatch: false;
error: null;
};
const initialState: ErrorBoundaryState = {
didCatch: false,
error: null,
};
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 };
}
resetErrorBoundary(...args: any[]) {
const { error } = this.state;
if (error !== null) {
this.props.onReset?.({
args,
reason: "imperative-api",
});
this.setState(initialState);
}
}
componentDidCatch(error: Error, info: ErrorInfo) {
this.props.onError?.(error, info);
}
componentDidUpdate(
prevProps: ErrorBoundaryProps,
prevState: ErrorBoundaryState
) {
const { didCatch } = this.state;
const { resetKeys } = this.props;
// There's an edge case where if the thing that triggered the error happens to *also* be in the resetKeys array,
// we'd end up resetting the error boundary immediately.
// This would likely trigger a second error to be thrown.
// So we make sure that we don't check the resetKeys on the first call of cDU after the error is set.
if (
didCatch &&
prevState.error !== null &&
hasArrayChanged(prevProps.resetKeys, resetKeys)
) {
this.props.onReset?.({
next: resetKeys,
prev: prevProps.resetKeys,
reason: "keys",
});
this.setState(initialState);
}
}
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 (isValidElement(fallback)) {
childToRender = fallback;
} else if (typeof fallbackRender === "function") {
childToRender = fallbackRender(props);
} else if (FallbackComponent) {
childToRender = createElement(FallbackComponent, props);
} else {
if (isDevelopment) {
console.error(
"react-error-boundary requires either a fallback, fallbackRender, or FallbackComponent prop"
);
}
throw error;
}
}
return createElement(
ErrorBoundaryContext.Provider,
{
value: {
didCatch,
error,
resetErrorBoundary: this.resetErrorBoundary,
},
},
childToRender
);
}
}
function hasArrayChanged(a: any[] = [], b: any[] = []) {
return (
a.length !== b.length || a.some((item, index) => !Object.is(item, b[index]))
);
}