Skip to content

Commit c906392

Browse files
authored
enhance: Improve ErrorBoundary (#3010)
1 parent 1b66aed commit c906392

38 files changed

+804
-341
lines changed

.changeset/fair-terms-sneeze.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@data-client/react": patch
3+
---
4+
5+
ErrorBoundary listens to all errors
6+
7+
This means it may catch errors that were previously passing thorugh

.changeset/metal-beds-compare.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@data-client/react": patch
3+
---
4+
5+
ErrorBoundary default error fallback supports react native

.changeset/shiny-windows-care.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
"@data-client/react": patch
3+
---
4+
5+
Add listen prop to ErrorBoundary and AsyncBoundary
6+
7+
```tsx
8+
<AsyncBoundary listen={history.listen}>
9+
<MatchedRoute index={0} />
10+
</AsyncBoundary>
11+
```

.changeset/weak-days-perform.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
"@data-client/react": patch
3+
---
4+
5+
Add resetErrorBoundary sent to errorComponent
6+
7+
```tsx
8+
function ErrorComponent({
9+
error,
10+
className,
11+
resetErrorBoundary,
12+
}: {
13+
error: Error;
14+
resetErrorBoundary: () => void;
15+
className?: string;
16+
}) {
17+
return (
18+
<pre role="alert" className={className}>
19+
{error.message} <button onClick={resetErrorBoundary}>Reset</button>
20+
</pre>
21+
);
22+
}
23+
```

.circleci/config.yml

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ jobs:
8787
./codecov < ./lcov.info || true;
8888
fi
8989
else
90-
yarn test:ci --maxWorkers=3 packages/react packages/redux packages/ssr packages/use-enhanced-reducer packages/img packages/rest packages/hooks
90+
yarn test:ci --maxWorkers=3
9191
fi
9292
9393
node_matrix:
@@ -130,9 +130,6 @@ jobs:
130130
if [ "<< parameters.typescript-version >>" != "latest" ]; then
131131
yarn add --dev typescript@<< parameters.typescript-version >>
132132
fi
133-
if [ "<< parameters.typescript-version >>" == "~4.0" ]; then
134-
yarn add --dev @types/[email protected]
135-
fi
136133
- run:
137134
name: typecheck
138135
command: |

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,7 @@ For the small price of 9kb gziped. &nbsp;&nbsp; [🏁Get started now](https://da
255255
- [ctrl.resolve](https://dataclient.io/docs/api/Controller#resolve)
256256
- [ctrl.subscribe](https://dataclient.io/docs/api/Controller#subscribe)
257257
- [ctrl.unsubscribe](https://dataclient.io/docs/api/Controller#unsubscribe)
258-
- Components: [&lt;CacheProvider/>](https://dataclient.io/docs/api/CacheProvider), [&lt;AsyncBoundary/>](https://dataclient.io/docs/api/AsyncBoundary), [&lt;NetworkErrorBoundary/>](https://dataclient.io/docs/api/NetworkErrorBoundary), [&lt;MockResolver/>](https://dataclient.io/docs/api/MockResolver)
258+
- Components: [&lt;CacheProvider/>](https://dataclient.io/docs/api/CacheProvider), [&lt;AsyncBoundary/>](https://dataclient.io/docs/api/AsyncBoundary), [&lt;ErrorBoundary/>](https://dataclient.io/docs/api/ErrorBoundary), [&lt;MockResolver/>](https://dataclient.io/docs/api/MockResolver)
259259
- Data Mocking: [Fixture](https://dataclient.io/docs/api/Fixtures#successfixture), [Interceptor](https://dataclient.io/docs/api/Fixtures#interceptor), [renderDataClient()](https://dataclient.io/docs/api/makeRenderDataClient)
260260
- Middleware: [LogoutManager](https://dataclient.io/docs/api/LogoutManager), [NetworkManager](https://dataclient.io/docs/api/NetworkManager), [SubscriptionManager](https://dataclient.io/docs/api/SubscriptionManager), [PollingSubscription](https://dataclient.io/docs/api/PollingSubscription), [DevToolsManager](https://dataclient.io/docs/api/DevToolsManager)
261261

docs/core/api/AsyncBoundary.md

Lines changed: 43 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,7 @@ title: '<AsyncBoundary />'
99

1010
Handles loading and error conditions of Suspense.
1111

12-
In React 18, this will create a concurrent split, and in 16 and 17 it will show loading fallbacks. If there is an
13-
irrecoverable API error, it will show an error fallback.
12+
In React 18, this will create a [concurrent split](https://react.dev/reference/react/useTransition), and in 16 and 17 it will show loading fallbacks. If there is an irrecoverable error, it will show an error fallback.
1413

1514
:::tip
1615

@@ -47,10 +46,13 @@ function SuspendingComponent() {
4746
interface BoundaryProps {
4847
children: React.ReactNode;
4948
fallback?: React.ReactNode;
49+
errorClassName?: string;
5050
errorComponent?: React.ComponentType<{
5151
error: NetworkError;
52+
resetErrorBoundary: () => void;
5253
className?: string;
5354
}>;
55+
listen?: (resetListener: () => void) => () => void;
5456
}
5557
```
5658

@@ -66,23 +68,21 @@ Component to handle caught errors
6668

6769
```tsx
6870
import React from 'react';
69-
import {
70-
CacheProvider,
71-
AsyncBoundary,
72-
NetworkError,
73-
} from '@data-client/react';
71+
import { CacheProvider, AsyncBoundary } from '@data-client/react';
7472

7573
function ErrorPage({
7674
error,
7775
className,
76+
resetErrorBoundary,
7877
}: {
79-
error: NetworkError;
78+
error: Error;
79+
resetErrorBoundary: () => void;
8080
className?: string;
8181
}) {
8282
return (
83-
<div className={className}>
84-
{error.status} {error.response && error.response.statusText}
85-
</div>
83+
<pre role="alert" className={className}>
84+
{error.message} <button onClick={resetErrorBoundary}>Reset</button>
85+
</pre>
8686
);
8787
}
8888

@@ -97,11 +97,37 @@ export default function App() {
9797
}
9898
```
9999

100-
Note: Once `<AsyncBoundary />` catches an error it will only render the fallback
101-
until it is remounted. To get around this you'll likely want to place the boundary at
102-
locations that will cause remounts when the error should be cleared. This is usually
103-
below the route itself.
104-
105-
## errorClassName
100+
### errorClassName
106101

107102
`className` to forward to [errorComponent](#errorcomponent)
103+
104+
### listen
105+
106+
Subscription handler to reset error state on events like URL location changes. This is great
107+
for placing a boundary to wrap routing components.
108+
109+
An example using [Anansi Router](https://www.npmjs.com/package/@anansi/router), which uses
110+
[history](https://www.npmjs.com/package/history) subscription.
111+
112+
```tsx
113+
import { useController } from '@anansi/router';
114+
import { AsyncBoundary } from '@data-client/react';
115+
116+
function App() {
117+
const { history } = useController();
118+
return (
119+
<div>
120+
<nav>
121+
<Link name="Home">Coin App</Link>
122+
</nav>
123+
<main>
124+
// highlight-start
125+
<AsyncBoundary listen={history.listen}>
126+
<MatchedRoute index={0} />
127+
</AsyncBoundary>
128+
// highlight-end
129+
</main>
130+
</div>
131+
);
132+
}
133+
```

docs/core/api/ErrorBoundary.md

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
---
2+
title: '<ErrorBoundary />'
3+
---
4+
5+
Displays a fallback component an error is thrown (including rejected [useSuspense()](./useSuspense.md)).
6+
7+
:::info
8+
9+
Reusable React error boundary component.
10+
11+
:::
12+
13+
## Usage
14+
15+
Place `ErrorBoundary` [at or above navigational boundaries](../getting-started/data-dependency.md#boundaries) like **pages, routes, or modals** to "catch" errors and render a fallback UI.
16+
17+
```tsx
18+
import React from 'react';
19+
import { ErrorBoundary } from '@data-client/react';
20+
21+
export default function MyPage() {
22+
return (
23+
<ErrorBoundary>
24+
<SuspendingComponent />
25+
</ErrorBoundary>
26+
);
27+
}
28+
29+
function SuspendingComponent() {
30+
const data = useSuspense(MyEndpoint);
31+
32+
return <div>{data.text}</div>;
33+
}
34+
```
35+
36+
## Props
37+
38+
```tsx
39+
interface Props {
40+
children: React.ReactNode;
41+
className?: string;
42+
fallbackComponent: React.ComponentType<{
43+
error: E;
44+
resetErrorBoundary: () => void;
45+
className?: string;
46+
}>;
47+
listen?: (resetListener: () => void) => () => void;
48+
}
49+
```
50+
51+
### fallbackComponent
52+
53+
```tsx
54+
import React from 'react';
55+
import { CacheProvider, ErrorBoundary } from '@data-client/react';
56+
57+
function ErrorPage({
58+
error,
59+
className,
60+
resetErrorBoundary,
61+
}: {
62+
error: Error;
63+
resetErrorBoundary: () => void;
64+
className?: string;
65+
}) {
66+
return (
67+
<pre role="alert" className={className}>
68+
{error.message} <button onClick={resetErrorBoundary}>Reset</button>
69+
</pre>
70+
);
71+
}
72+
73+
export default function App() {
74+
return (
75+
<CacheProvider>
76+
<ErrorBoundary fallbackComponent={ErrorPage} className="error">
77+
<Router />
78+
</ErrorBoundary>
79+
</CacheProvider>
80+
);
81+
}
82+
```
83+
84+
### listen
85+
86+
Subscription handler to reset error state on events like URL location changes. This is great
87+
for placing a boundary to wrap routing components.
88+
89+
An example using [Anansi Router](https://www.npmjs.com/package/@anansi/router), which uses
90+
[history](https://www.npmjs.com/package/history) subscription.
91+
92+
```tsx
93+
import { useController } from '@anansi/router';
94+
import { ErrorBoundary } from '@data-client/react';
95+
96+
function App() {
97+
const { history } = useController();
98+
return (
99+
<div>
100+
<nav>
101+
<Link name="Home">Coin App</Link>
102+
</nav>
103+
<main>
104+
// highlight-start
105+
<ErrorBoundary listen={history.listen}>
106+
<MatchedRoute index={0} />
107+
</ErrorBoundary>
108+
// highlight-end
109+
</main>
110+
</div>
111+
);
112+
}
113+
```
114+
115+
### className
116+
117+
`className` to forward to [fallbackComponent](#fallbackcomponent)

docs/core/api/NetworkErrorBoundary.md

Lines changed: 0 additions & 72 deletions
This file was deleted.

docs/core/concepts/error-policy.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ It uses the rejection error to determine whether it should be treated as 'soft'
1818

1919
Soft errors will continue showing valid data if it exists. However, if no previous data is in the store,
2020
it will reject with `error`. In this case [useSuspense()](../api/useSuspense.md) throws the
21-
error to be caught by the nearest [NetworkErrorBoundary](../api/NetworkErrorBoundary.md) or [AsyncBoundary](../api/AsyncBoundary.md)
21+
error to be caught by the nearest [ErrorBoundary](../api/ErrorBoundary.md) or [AsyncBoundary](../api/AsyncBoundary.md)
2222

2323
### Hard
2424

0 commit comments

Comments
 (0)