Skip to content

Commit 4e240df

Browse files
authored
feat(reset): add onReset and resetKeys props (#50)
This allows you to declaritively specify what you want to have happen when a state reset occurs and also specify when a reset should take place if a given prop changes.
1 parent 8c733a0 commit 4e240df

File tree

4 files changed

+182
-19
lines changed

4 files changed

+182
-19
lines changed

README.md

Lines changed: 118 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,15 @@ then be gracefully handled.
3636
- [Installation](#installation)
3737
- [Usage](#usage)
3838
- [Error Recovery](#error-recovery)
39-
- [fallback prop](#fallback-prop)
39+
- [API](#api)
40+
- [`ErrorBoundary` props](#errorboundary-props)
41+
- [`children`](#children)
42+
- [`FallbackComponent`](#fallbackcomponent)
43+
- [`fallbackRender`](#fallbackrender)
44+
- [`fallback`](#fallback)
45+
- [`onError`](#onerror)
46+
- [`onReset`](#onreset)
47+
- [`resetKeys`](#resetkeys)
4048
- [Issues](#issues)
4149
- [🐛 Bugs](#-bugs)
4250
- [💡 Feature Requests](#-feature-requests)
@@ -62,20 +70,26 @@ its descendants too.
6270
```jsx
6371
import {ErrorBoundary} from 'react-error-boundary'
6472

65-
function ErrorFallback({error, componentStack}) {
73+
function ErrorFallback({error, componentStack, resetErrorBoundary}) {
6674
return (
6775
<div role="alert">
6876
<p>Something went wrong:</p>
6977
<pre>{error.message}</pre>
7078
<pre>{componentStack}</pre>
79+
<button onClick={resetErrorBoundary}>Try again</button>
7180
</div>
7281
)
7382
}
7483

7584
const ui = (
76-
<ErrorBoundary FallbackComponent={ErrorFallback}>
85+
<ErrorBoundary
86+
FallbackComponent={ErrorFallback}
87+
onReset={() => {
88+
// reset the state of your app so the error doesn't happen again
89+
}}
90+
>
7791
<ComponentThatMayError />
78-
</ErrorBoundary>,
92+
</ErrorBoundary>
7993
)
8094
```
8195

@@ -115,27 +129,89 @@ const ui = <ComponentWithErrorBoundary />
115129

116130
### Error Recovery
117131

118-
Often you may want to recover from the error. You can do this using the
119-
`resetErrorBoundary` prop:
132+
In the event of an error if you want to recover from that error and allow the
133+
user to "try again" or continue with their work, you'll need a way to reset the
134+
ErrorBoundary's internal state. You can do this various ways, but here's the
135+
most idiomatic approach:
120136

121137
```jsx
122-
function ErrorFallback({error, resetErrorBoundary}) {
138+
function ErrorFallback({error, componentStack, resetErrorBoundary}) {
123139
return (
124140
<div role="alert">
125-
<div>Oh no</div>
141+
<p>Something went wrong:</p>
126142
<pre>{error.message}</pre>
143+
<pre>{componentStack}</pre>
127144
<button onClick={resetErrorBoundary}>Try again</button>
128145
</div>
129146
)
130147
}
148+
149+
function Bomb() {
150+
throw new Error('💥 CABOOM 💥')
151+
}
152+
153+
function App() {
154+
const [explode, setExplode] = React.useState(false)
155+
return (
156+
<div>
157+
<button onClick={() => setExplode(e => !e)}>toggle explode</button>
158+
<ErrorBoundary
159+
FallbackComponent={ErrorFallback}
160+
onReset={() => setExplode(false)}
161+
resetKeys={[explode]}
162+
>
163+
{explode ? <Bomb /> : null}
164+
</ErrorBoundary>
165+
</div>
166+
)
167+
}
131168
```
132169

133-
However, normally "trying again" like that will just result in the user
134-
experiencing the same error. Typically some other state in your app will need to
135-
be reset as well. The problem is, the `ErrorFallback` component won't usually
136-
have access to the state that needs to be reset.
170+
So, with this setup, you've got a button which when clicked will trigger an
171+
error. Clicking the button again will trigger a re-render which recovers from
172+
the error (we no longer render the `<Bomb />`). We also pass the `resetKeys`
173+
prop which is an array of elements for the `ErrorBoundary` to check each render
174+
(if there's currently an error state). If any of those elements change between
175+
renders, then the `ErrorBoundary` will reset the state which will re-render the
176+
children.
177+
178+
We have the `onReset` prop so that if the user clicks the "Try again" button we
179+
have an opportunity to re-initialize our state into a good place before
180+
attempting to re-render the children.
181+
182+
This combination allows us both the opportunity to give the user something
183+
specific to do to recover from the error, and recover from the error by
184+
interacting with other areas of the app that might fix things for us. It's hard
185+
to describe here, but hopefully it makes sense when you apply it to your
186+
specific scenario.
187+
188+
## API
137189

138-
So alternatively, you can use the `fallbackRender` prop:
190+
### `ErrorBoundary` props
191+
192+
### `children`
193+
194+
This is what you want rendered when everything's working fine. If there's an
195+
error that React can handle within the children of the `ErrorBoundary`, the
196+
`ErrorBoundary` will catch that and allow you to handle it gracefully.
197+
198+
### `FallbackComponent`
199+
200+
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).
204+
205+
This is required if no `fallback` or `fallbackRender` prop is provided.
206+
207+
### `fallbackRender`
208+
209+
This is a render-prop based API that allows you to inline your error fallback UI
210+
into the component that's using the `ErrorBoundary`. This is useful if you need
211+
access to something that's in the scope of the component you're using.
212+
213+
It will be called with an object that has `error`, `componentStack`, and
214+
`resetErrorBoundary`:
139215

140216
```jsx
141217
const ui = (
@@ -146,7 +222,10 @@ const ui = (
146222
<pre>{error.message}</pre>
147223
<button
148224
onClick={() => {
149-
resetComponentState() // <-- this is why the fallbackRender is useful
225+
// this next line is why the fallbackRender is useful
226+
resetComponentState()
227+
// though you could accomplish this with a combination
228+
// of the FallbackCallback and onReset props as well.
150229
resetErrorBoundary()
151230
}}
152231
>
@@ -165,7 +244,9 @@ around. Unfortunately, the current React Error Boundary API only supports class
165244
components at the moment, so render props are the best solution we have to this
166245
problem.
167246

168-
### fallback prop
247+
This is required if no `FallbackComponent` or `fallback` prop is provided.
248+
249+
### `fallback`
169250

170251
In the spirit of consistency with the `React.Suspense` component, we also
171252
support a simple `fallback` prop which you can use for a generic fallback. This
@@ -180,6 +261,28 @@ const ui = (
180261
)
181262
```
182263

264+
### `onError`
265+
266+
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`.
268+
269+
### `onReset`
270+
271+
This will be called immediately before the `ErrorBoundary` resets it's internal
272+
state (which will result in rendering the `children` again). You should use this
273+
to ensure that re-rendering the children will not result in a repeat of the same
274+
error happening again.
275+
276+
### `resetKeys`
277+
278+
Sometimes an error happens as a result of local state to the component that's
279+
rendering the error. If this is the case, then you can pass `resetKeys` which is
280+
an array of values. If the `ErrorBoundary` is in an error state, then it will
281+
check these values each render and if they change from one render to the next,
282+
then it will reset automatically (triggering a re-render of the `children`).
283+
284+
See the recovery examples above.
285+
183286
## Issues
184287

185288
_Looking to contribute? Look for the [Good First Issue][good-first-issue]

index.d.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,32 @@ export interface FallbackProps {
77
}
88

99
export interface ErrorBoundaryPropsWithComponent {
10+
onReset?: () => void
1011
onError?: (error: Error, componentStack: string) => void
12+
resetKeys?: Array<any>
1113
FallbackComponent: React.ComponentType<FallbackProps>
1214
}
1315

1416
export interface ErrorBoundaryPropsWithRender {
17+
onReset?: () => void
1518
onError?: (error: Error, componentStack: string) => void
19+
resetKeys?: Array<any>
1620
fallbackRender: (props: FallbackProps) => React.ReactElement<any, any> | null
1721
}
1822

23+
export interface ErrorBoundaryPropsWithFallback {
24+
onReset?: () => void
25+
onError?: (error: Error, componentStack: string) => void
26+
resetKeys?: Array<any>
27+
fallback: React.ReactElement<any, any> | null
28+
}
29+
1930
export type ErrorBoundaryProps =
31+
| ErrorBoundaryPropsWithFallback
2032
| ErrorBoundaryPropsWithComponent
2133
| ErrorBoundaryPropsWithRender
2234

23-
export class ErrorBoundary extends React.Component<
24-
ErrorBoundaryProps
25-
> {}
35+
export class ErrorBoundary extends React.Component<ErrorBoundaryProps> {}
2636

2737
export function withErrorBoundary<P>(
2838
ComponentToDecorate: React.ComponentType<P>,

src/__tests__/index.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,42 @@ test('requires either a fallback, fallbackRender, or FallbackComponent', () => {
251251
console.error.mockClear()
252252
})
253253

254+
test('supports automatic reset of error boundary when resetKeys change', () => {
255+
function App() {
256+
const [explode, setExplode] = React.useState(false)
257+
return (
258+
<div>
259+
<button onClick={() => setExplode(e => !e)}>toggle explode</button>
260+
<ErrorBoundary
261+
FallbackComponent={ErrorFallback}
262+
onReset={() => setExplode(false)}
263+
resetKeys={[explode]}
264+
>
265+
{explode ? <Bomb /> : null}
266+
</ErrorBoundary>
267+
</div>
268+
)
269+
}
270+
render(<App />)
271+
userEvent.click(screen.getByText('toggle explode'))
272+
273+
screen.getByRole('alert')
274+
expect(console.error).toHaveBeenCalledTimes(2)
275+
console.error.mockClear()
276+
277+
userEvent.click(screen.getByText(/try again/i))
278+
expect(screen.queryByRole('alert')).not.toBeInTheDocument()
279+
280+
userEvent.click(screen.getByText('toggle explode'))
281+
screen.getByRole('alert')
282+
expect(console.error).toHaveBeenCalledTimes(2)
283+
console.error.mockClear()
284+
285+
userEvent.click(screen.getByText('toggle explode'))
286+
expect(screen.queryByRole('alert')).not.toBeInTheDocument()
287+
expect(console.error).not.toHaveBeenCalled()
288+
})
289+
254290
/*
255291
eslint
256292
no-console: "off",

src/index.js

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,29 @@
11
import React from 'react'
22

3+
const changedArray = (a = [], b = []) =>
4+
a.some((item, index) => !Object.is(item, b[index]))
5+
36
const initialState = {error: null, info: null}
47
class ErrorBoundary extends React.Component {
58
state = initialState
6-
resetErrorBoundary = () => this.setState(initialState)
9+
resetErrorBoundary = () => {
10+
this.props.onReset?.()
11+
this.setState(initialState)
12+
}
713

814
componentDidCatch(error, info) {
915
this.props.onError?.(error, info?.componentStack)
1016
this.setState({error, info})
1117
}
1218

19+
componentDidUpdate(prevProps) {
20+
const {error} = this.state
21+
const {resetKeys} = this.props
22+
if (error !== null && changedArray(prevProps.resetKeys, resetKeys)) {
23+
this.resetErrorBoundary()
24+
}
25+
}
26+
1327
render() {
1428
const {error, info} = this.state
1529
const {fallbackRender, FallbackComponent, fallback} = this.props

0 commit comments

Comments
 (0)