Skip to content

Commit b2f82ce

Browse files
authored
fix: avoid double-render (#68)
BREAKING CHANGE: This removes the `componentStack` in the props given to the `FallbackComponent` and `fallbackRender`
1 parent 463c62e commit b2f82ce

File tree

3 files changed

+38
-57
lines changed

3 files changed

+38
-57
lines changed

README.md

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -70,12 +70,11 @@ its descendants too.
7070
```jsx
7171
import {ErrorBoundary} from 'react-error-boundary'
7272

73-
function ErrorFallback({error, componentStack, resetErrorBoundary}) {
73+
function ErrorFallback({error, resetErrorBoundary}) {
7474
return (
7575
<div role="alert">
7676
<p>Something went wrong:</p>
7777
<pre>{error.message}</pre>
78-
<pre>{componentStack}</pre>
7978
<button onClick={resetErrorBoundary}>Try again</button>
8079
</div>
8180
)
@@ -98,7 +97,7 @@ You can react to errors (e.g. for logging) by providing an `onError` callback:
9897
```jsx
9998
import {ErrorBoundary} from 'react-error-boundary'
10099

101-
const myErrorHandler = (error: Error, componentStack: string) => {
100+
const myErrorHandler = (error: Error, info: {componentStack: string}) => {
102101
// Do something with the error
103102
// E.g. log to an error logging client here
104103
}
@@ -118,7 +117,7 @@ import {withErrorBoundary} from 'react-error-boundary'
118117

119118
const ComponentWithErrorBoundary = withErrorBoundary(ComponentThatMayError, {
120119
FallbackComponent: ErrorBoundaryFallbackComponent,
121-
onError(error, componentStack) {
120+
onError(error, info) {
122121
// Do something with the error
123122
// E.g. log to an error logging client here
124123
},
@@ -135,12 +134,11 @@ ErrorBoundary's internal state. You can do this various ways, but here's the
135134
most idiomatic approach:
136135

137136
```jsx
138-
function ErrorFallback({error, componentStack, resetErrorBoundary}) {
137+
function ErrorFallback({error, resetErrorBoundary}) {
139138
return (
140139
<div role="alert">
141140
<p>Something went wrong:</p>
142141
<pre>{error.message}</pre>
143-
<pre>{componentStack}</pre>
144142
<button onClick={resetErrorBoundary}>Try again</button>
145143
</div>
146144
)
@@ -198,9 +196,9 @@ error that React can handle within the children of the `ErrorBoundary`, the
198196
#### `FallbackComponent`
199197

200198
This is a component you want rendered in the event of an error. As props it will
201-
be passed the `error`, `componentStack`, and `resetErrorBoundary` (which will
202-
reset the error boundary's state when called, useful for a "try again" button
203-
when used in combination with the `onReset` prop).
199+
be passed the `error` and `resetErrorBoundary` (which will reset the error
200+
boundary's state when called, useful for a "try again" button when used in
201+
combination with the `onReset` prop).
204202

205203
This is required if no `fallback` or `fallbackRender` prop is provided.
206204

@@ -210,8 +208,7 @@ This is a render-prop based API that allows you to inline your error fallback UI
210208
into the component that's using the `ErrorBoundary`. This is useful if you need
211209
access to something that's in the scope of the component you're using.
212210

213-
It will be called with an object that has `error`, `componentStack`, and
214-
`resetErrorBoundary`:
211+
It will be called with an object that has `error` and `resetErrorBoundary`:
215212

216213
```jsx
217214
const ui = (
@@ -264,7 +261,7 @@ const ui = (
264261
#### `onError`
265262

266263
This will be called when there's been an error that the `ErrorBoundary` has
267-
handled. It will be called with two arguments: `error`, `componentStack`.
264+
handled. It will be called with two arguments: `error`, `info`.
268265

269266
#### `onReset`
270267

src/__tests__/index.js

Lines changed: 5 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,11 @@ import {render, screen} from '@testing-library/react'
33
import userEvent from '@testing-library/user-event'
44
import {ErrorBoundary, withErrorBoundary} from '..'
55

6-
function ErrorFallback({error, componentStack, resetErrorBoundary}) {
6+
function ErrorFallback({error, resetErrorBoundary}) {
77
return (
88
<div role="alert">
99
<p>Something went wrong:</p>
1010
<pre>{error.message}</pre>
11-
<pre>{componentStack}</pre>
1211
<button onClick={resetErrorBoundary}>Try again</button>
1312
</div>
1413
)
@@ -73,14 +72,6 @@ test('standard use-case', async () => {
7372
<pre>
7473
💥 CABOOM 💥
7574
</pre>
76-
<pre>
77-
78-
in Bomb
79-
in ErrorBoundary
80-
in div
81-
in div
82-
in Unknown
83-
</pre>
8475
<button>
8576
Try again
8677
</button>
@@ -170,10 +161,12 @@ test('withErrorBoundary HOC', () => {
170161
const [error, onErrorComponentStack] = onErrorHandler.mock.calls[0]
171162
expect(error.message).toMatchInlineSnapshot(`"💥 CABOOM 💥"`)
172163
expect(onErrorComponentStack).toMatchInlineSnapshot(`
173-
"
164+
Object {
165+
"componentStack": "
174166
in Unknown (created by withErrorBoundary(Unknown))
175167
in ErrorBoundary (created by withErrorBoundary(Unknown))
176-
in withErrorBoundary(Unknown)"
168+
in withErrorBoundary(Unknown)",
169+
}
177170
`)
178171
expect(onErrorHandler).toHaveBeenCalledTimes(1)
179172
})
@@ -222,18 +215,6 @@ test('requires either a fallback, fallbackRender, or FallbackComponent', () => {
222215
).toThrowErrorMatchingInlineSnapshot(
223216
`"react-error-boundary requires either a fallback, fallbackRender, or FallbackComponent prop"`,
224217
)
225-
const [, , [actualError], [componentStack]] = console.error.mock.calls
226-
expect(firstLine(actualError)).toMatchInlineSnapshot(
227-
`"Error: Uncaught [Error: react-error-boundary requires either a fallback, fallbackRender, or FallbackComponent prop]"`,
228-
)
229-
expect(componentStack).toMatchInlineSnapshot(`
230-
"The above error occurred in the <ErrorBoundary> component:
231-
in ErrorBoundary
232-
233-
Consider adding an error boundary to your tree to customize error handling behavior.
234-
Visit https://fb.me/react-error-boundaries to learn more about error boundaries."
235-
`)
236-
expect(console.error).toHaveBeenCalledTimes(4)
237218
console.error.mockClear()
238219
})
239220

src/index.js

Lines changed: 24 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,52 +3,55 @@ import React from 'react'
33
const changedArray = (a = [], b = []) =>
44
a.length !== b.length || a.some((item, index) => !Object.is(item, b[index]))
55

6-
const initialState = {error: null, info: null}
6+
const initialState = {error: null}
77
class ErrorBoundary extends React.Component {
88
static getDerivedStateFromError(error) {
99
return {error}
1010
}
1111

1212
state = initialState
13+
updatedWithError = false
1314
resetErrorBoundary = (...args) => {
1415
this.props.onReset?.(...args)
16+
this.reset()
17+
}
18+
19+
reset() {
20+
this.updatedWithError = false
1521
this.setState(initialState)
1622
}
1723

1824
componentDidCatch(error, info) {
19-
this.props.onError?.(error, info?.componentStack)
20-
this.setState({info})
25+
this.props.onError?.(error, info)
2126
}
2227

2328
componentDidUpdate(prevProps) {
24-
const {error, info} = this.state
29+
const {error} = this.state
2530
const {resetKeys} = this.props
26-
if (
27-
error !== null &&
28-
info !== null &&
29-
changedArray(prevProps.resetKeys, resetKeys)
30-
) {
31+
32+
// There's an edge case where if the thing that triggered the error
33+
// happens to *also* be in the resetKeys array, we'd end up resetting
34+
// the error boundary immediately. This would likely trigger a second
35+
// error to be thrown.
36+
// So we make sure that we don't check the resetKeys on the first call
37+
// of cDU after the error is set
38+
if (error !== null && !this.updatedWithError) {
39+
this.updatedWithError = true
40+
return
41+
}
42+
43+
if (error !== null && changedArray(prevProps.resetKeys, resetKeys)) {
3144
this.props.onResetKeysChange?.(prevProps.resetKeys, resetKeys)
32-
this.setState(initialState)
45+
this.reset()
3346
}
3447
}
3548

3649
render() {
37-
const {error, info} = this.state
50+
const {error} = this.state
3851
const {fallbackRender, FallbackComponent, fallback} = this.props
3952

4053
if (error !== null) {
41-
// we'll get a re-render with the error state in getDerivedStateFromError
42-
// but we don't have the info yet, so just render null
43-
// note that this won't be committed to the DOM thanks to our componentDidCatch
44-
// so the user won't see a flash of nothing, so this works fine.
45-
// the benefit of doing things this way rather than just putting both the
46-
// error and info setState within componentDidCatch is we avoid re-rendering
47-
// busted stuff: https://github.com/bvaughn/react-error-boundary/issues/66
48-
if (!info) return null
49-
5054
const props = {
51-
componentStack: info?.componentStack,
5255
error,
5356
resetErrorBoundary: this.resetErrorBoundary,
5457
}

0 commit comments

Comments
 (0)