Skip to content

Commit 09f24d7

Browse files
Merge pull request #24 from JoschuaSchneider/reset-error-state
[Release 2.0.5] Reset error state
2 parents 229b9da + 063fd36 commit 09f24d7

File tree

6 files changed

+160
-45
lines changed

6 files changed

+160
-45
lines changed

README.md

Lines changed: 60 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,33 @@
11
# use-error-boundary
22

33
[![npm version](https://img.shields.io/npm/v/use-error-boundary.svg)](https://www.npmjs.com/package/use-error-boundary)
4-
![build status](https://travis-ci.org/JoschuaSchneider/use-error-boundary.svg?branch=master)
4+
![TypeScript](https://badgen.net/badge/-/TypeScript/blue?icon=typescript&label)
5+
![build workflow](https://github.com/JoschuaSchneider/use-error-boundary/actions/workflows/build.yml/badge.svg?branch=master)
6+
![test workflow](https://github.com/JoschuaSchneider/use-error-boundary/actions/workflows/test.yml/badge.svg?branch=master)
57
![license](https://img.shields.io/npm/l/use-error-boundary.svg)
68

79
A **react hook** for using error boundaries in your functional components.
810

9-
It lets you keep track of the error state of child components, by wrapping them in a provided `ErrorBoundary` component.
11+
It lets you keep track of the error state of child components, by wrapping them in the provided `ErrorBoundary` component.
1012

11-
:warning: Read more about error boundaries and their intended use in the [React documentation](https://reactjs.org/docs/error-boundaries.html), this will only catch errors when rendering your children!
13+
:warning: Read more about error boundaries and their intended use in the [React documentation](https://reactjs.org/docs/error-boundaries.html), this will only catch errors during the render phase!
1214

1315
### Installation
1416

1517
```bash
1618
npm i use-error-boundary
1719
```
1820

21+
```bash
22+
yarn add use-error-boundary
23+
```
24+
1925
### Breaking changes in `2.x`
2026

21-
If you are upgrading from version `1.x` please make sure you are not using the `errorInfo` object.
22-
The hook itself and the `renderError` callback no longer provide this object.
27+
While upgrading from version `1.x` make sure you are not using the `errorInfo` object.
28+
The hook and the `renderError` callback no longer provide this object.
2329

24-
For advanced use, please refer to [Custom handling of error and errorInfo](#custom-handling-of-error-and-errorinfo).
30+
For advanced use, please refer to [Custom handling of error and errorInfo](#handling-error-and-errorinfo-outside-of-markup).
2531

2632
## Examples and usage
2733

@@ -34,28 +40,30 @@ import { useErrorBoundary } from "use-error-boundary"
3440
import useErrorBoundary from "use-error-boundary"
3541
```
3642

37-
Please read more info on the [returned properties](#returned-properties) by the hook.
43+
Learn more about the [properties that are returned](#returned-properties).
3844

3945
```javascript
4046
const MyComponent = () => {
4147

4248
const {
4349
ErrorBoundary,
4450
didCatch,
45-
error
51+
error,
52+
reset
4653
} = useErrorBoundary()
4754

48-
...
55+
//...
4956

5057
}
5158
```
5259

53-
### Use without render props
60+
### Usage without render props
5461

55-
Wrap your components in the provided `ErrorBoundary`,
56-
if it catches an error the hook provides you with the changed state and the boundary Component will render nothing. So you have to handle rendering some error display yourself.
62+
Wrap your components in the provided `ErrorBoundary`.
63+
When it catches an error the hook provides you the changed error-state and the boundary Component will render nothing.
64+
You have to handle rendering some error display yourself.
5765

58-
If you want the boundary to also render your error display, you can [use it with render props](#use-with-render-props)
66+
You can get the ErrorBoundary component to render your custom error display by [using the `renderError` render-prop.](#use-with-render-props)
5967

6068
```javascript
6169
const JustRenderMe = () => {
@@ -79,13 +87,13 @@ const MyComponent = () => {
7987
}
8088
```
8189

82-
### Use with render props
90+
### Usage with render props
8391

84-
Optionally, you can pass a `render` and `renderError` function to render the components to display errors in the boundary itself:
92+
Optionally, you can pass a `render` and `renderError` function to render your UI and error-state inside the boundary.
8593

8694
```javascript
8795
/**
88-
* The renderError function also passes the error and errorInfo, so that you can display it using
96+
* The renderError function also passes the error, so that you can display it using
8997
* render props.
9098
*/
9199
return (
@@ -96,9 +104,9 @@ return (
96104
)
97105
```
98106

99-
## Custom handling of `error` and `errorInfo`
107+
## Handling `error` and `errorInfo` outside of markup
100108

101-
The hook now accepts an `options` object that you can pass a `onDidCatch` callback that gets called when the ErrorBoundary catches an error.
109+
The hook now accepts an `options` object that you can pass a `onDidCatch` callback that gets called when the ErrorBoundary catches an error. Use this for logging or reporting of errors.
102110

103111
```js
104112
useErrorBoundary({
@@ -112,20 +120,46 @@ useErrorBoundary({
112120

113121
These are the properties of the returned Object:
114122

115-
| Property | Type | Description |
116-
| --------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
117-
| `ErrorBoundary` | React Component | Special error boundary component that provides state changes to the hook. <br>:warning: **You need to use this as the error boundary! Otherwise, the state will not update when errors are caught!** <br> The ErrorBoundary is **guaranteed referential equality** across rerenders. |
118-
| `didCatch` | Boolean | `true` if an error has been caught |
119-
| `error` | Error Object or `null` | The error caught by the Boundary |
123+
### `ErrorBoundary`
124+
`Type: React Component`
125+
126+
127+
Special error boundary component that provides state changes to the hook.
128+
129+
:warning: **You need to use this as the error boundary! Otherwise, the state will not update when errors are caught!**
130+
131+
The ErrorBoundary is **guaranteed referential equality** across rerenders and only updates after a reset.
132+
133+
### `didCatch`
134+
`Type: boolean`
135+
136+
The error state, `true` if an error has ben caught.
137+
138+
139+
### `error`
140+
`Type: any | null`
141+
142+
The error caught by the boundary, or `null`.
143+
144+
### `reset`
145+
`Type: function`
146+
147+
Function the reset the error state.
148+
Forces react to recreate the boundary by creating a new ErrorBoundary
149+
150+
Your boundary can now catch errors again.
120151

121152
If you are searching for the `errorInfo` property, please read [Breaking Changes in 2.x](#breaking-changes-in-2x).
122153

123-
## Why should I use this?
154+
## Why should I use this hook?
124155

125156
React does not provide a way to catch errors within the same functional component and you have to handle that in a class Component with special lifecycle methods.
126-
If you are new to ErrorBoundaries, I recommend implementing this yourself!
127157

128-
This packages purpose is to provide an easy drop in replacement for projects that are being migrated to hooks and to pull the error presentation out of the boundary itself by putting it on the same level you are catching the errors.
158+
If you are new to ErrorBoundaries, building this yourself is a good way to get started!
159+
160+
This packages purpose is to provide an easy drop in replacement for projects that are being migrated to hooks.
161+
162+
This also pulls the error presentation out of the error boundary, and on the same level you are handling errors.
129163

130164
## Contributing
131165

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "use-error-boundary",
3-
"version": "2.0.4",
3+
"version": "2.0.5",
44
"description": "React hook for using error boundaries",
55
"source": "src/index.ts",
66
"main": "lib/index.js",

src/ErrorBoundary.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ export class ErrorBoundary extends PureComponent<
6565
componentDidCatch(error: any, errorInfo: any) {
6666
return this.props.onDidCatch(error, errorInfo)
6767
}
68+
6869
/**
6970
* Render children or fallback ui depending on the error state.
7071
*

src/__tests__/use-error-boundary.spec.tsx

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,37 @@ function ClickToExplode(options?: UseErrorBoundaryOptions) {
4848
)
4949
}
5050

51+
// Use to test hook behaviour
52+
function ClickToExplodeAndReset(options?: UseErrorBoundaryOptions) {
53+
const [explode, setExplode] = useState(false)
54+
const { ErrorBoundary, didCatch, error, reset } = useErrorBoundary(options)
55+
56+
return (
57+
<>
58+
<div data-testid="boundary-inside">
59+
<ErrorBoundary>
60+
{explode ? (
61+
<Explosion />
62+
) : (
63+
<button onClick={() => setExplode(true)}>Explode!</button>
64+
)}
65+
</ErrorBoundary>
66+
</div>
67+
<p data-testid="didcatch">{didCatch ? "true" : "false"}</p>
68+
<p data-testid="error-message">{error?.message}</p>
69+
<button
70+
data-testid="reset-button"
71+
onClick={() => {
72+
setExplode(false)
73+
reset()
74+
}}
75+
>
76+
Reset
77+
</button>
78+
</>
79+
)
80+
}
81+
5182
// Use to test render props of wrapped ErrorBoundary
5283
function InstantExplosion(options?: UseErrorBoundaryOptions) {
5384
const { ErrorBoundary } = useErrorBoundary(options)
@@ -76,6 +107,7 @@ test("Hook initializes state", async () => {
76107
expect(result.current.didCatch).toBe(false)
77108
expect(result.current.ErrorBoundary).toBeDefined()
78109
expect(result.current.error).toBe(null)
110+
expect(result.current.reset).toBeDefined()
79111
})
80112

81113
test("Wrapped ErrorBoundary catches error", async () => {
@@ -100,6 +132,38 @@ test("Wrapped ErrorBoundary catches error", async () => {
100132
expect(console.error).toHaveBeenCalledTimes(2)
101133
})
102134

135+
test("Wrapped ErrorBoundary resets and catches again", async () => {
136+
const onDidCatch = jest.fn()
137+
138+
render(<ClickToExplodeAndReset onDidCatch={onDidCatch} />)
139+
140+
act(() => {
141+
fireEvent.click(screen.getByText("Explode!"))
142+
})
143+
144+
// Boundary should render nothing
145+
expect(screen.getByTestId("boundary-inside")).toBeEmptyDOMElement()
146+
147+
// Reset the boundary
148+
act(() => {
149+
fireEvent.click(screen.getByTestId("reset-button"))
150+
})
151+
152+
// Explode button should be back
153+
expect(screen.getByText("Explode!")).toBeInTheDocument()
154+
155+
// Generate a new error
156+
act(() => {
157+
fireEvent.click(screen.getByText("Explode!"))
158+
})
159+
160+
// We should now have catched two errors in total
161+
expect(onDidCatch).toBeCalledTimes(2)
162+
// React and testing-library calls console.error when a boundary catches, this should happen
163+
// 4 times if we catch 2 errors
164+
expect(console.error).toHaveBeenCalledTimes(4)
165+
})
166+
103167
test("Wrapped ErrorBoundary catches error with render props", async () => {
104168
const onDidCatch = jest.fn()
105169

src/use-error-boundary.ts

Lines changed: 33 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useRef, useReducer } from "react"
1+
import React, { useRef, useReducer, useCallback } from "react"
22

33
import {
44
createErrorBoundary,
@@ -12,10 +12,11 @@ export interface ErrorState {
1212

1313
export interface UseErrorBoundaryState extends ErrorState {
1414
ErrorBoundary: UseErrorBoundaryWrapper
15+
reset: () => void
1516
}
1617

1718
interface StateAction {
18-
type: "catch"
19+
type: "catch" | "reset"
1920
error?: any | null
2021
}
2122

@@ -50,11 +51,15 @@ const useErrorBoundaryReducer: UseErrorBoundaryReducer = (state, action) => {
5051
// The component did catch, update state
5152
case "catch":
5253
return {
53-
//...state,
5454
didCatch: true,
5555
// Pass the values from action.error
5656
error: action.error,
5757
}
58+
case "reset":
59+
return {
60+
didCatch: false,
61+
error: null,
62+
}
5863
// Unknown action, return state
5964
default:
6065
return state
@@ -76,18 +81,10 @@ function useErrorBoundary(
7681
// Create ref for wrapped ErrorBoundary class
7782
const errorBoundaryWrapperRef = useRef<UseErrorBoundaryWrapper | null>(null)
7883

79-
// Get the current ref value or initialize it with a new wrapped ErrorBoundary
80-
function getWrappedErrorBoundary() {
81-
// Get current ref value
82-
let errorBoundaryWrapper = errorBoundaryWrapperRef.current
83-
84-
// Return the component when already initialized
85-
if (errorBoundaryWrapper !== null) {
86-
return errorBoundaryWrapper
87-
}
88-
84+
// Create a new wrapped boundary
85+
function createWrappedErrorBoundary() {
8986
// Create new wrapped ErrorBoundary class with onDidCatch callback
90-
errorBoundaryWrapper = createErrorBoundary((err, errorInfo) => {
87+
return createErrorBoundary((err, errorInfo) => {
9188
// Dispatch action in case of an error
9289
dispatch({
9390
type: "catch",
@@ -97,19 +94,38 @@ function useErrorBoundary(
9794
// call onDidCatch if provided by user
9895
if (options && options.onDidCatch) options.onDidCatch(err, errorInfo)
9996
})
97+
}
98+
99+
// Get the current ref value or initialize it with a new wrapped ErrorBoundary
100+
function getWrappedErrorBoundary() {
101+
// Get current ref value
102+
let errorBoundaryWrapper = errorBoundaryWrapperRef.current
100103

101-
// Update the ref with new component
102-
errorBoundaryWrapperRef.current = errorBoundaryWrapper
104+
// Return the component when already initialized
105+
if (errorBoundaryWrapper !== null) {
106+
return errorBoundaryWrapper
107+
}
108+
109+
// Update the ref with new boundary
110+
errorBoundaryWrapperRef.current = createWrappedErrorBoundary()
103111

104112
// Return the newly created component
105-
return errorBoundaryWrapper
113+
return errorBoundaryWrapperRef.current
106114
}
107115

116+
const reset = useCallback(() => {
117+
// create a new wrapped boundary to force a rerender
118+
errorBoundaryWrapperRef.current = createWrappedErrorBoundary()
119+
// Reset the hooks error state
120+
dispatch({ type: "reset" })
121+
}, [])
122+
108123
// Return the wrapped ErrorBoundary class to wrap your components in plus the error state
109124
return {
110125
ErrorBoundary: getWrappedErrorBoundary(),
111126
didCatch: state.didCatch,
112127
error: state.error,
128+
reset,
113129
}
114130
}
115131

0 commit comments

Comments
 (0)