Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a global ErrorBoundary #9799

Merged
merged 12 commits into from
May 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 81 additions & 22 deletions docs/Admin.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,28 +136,29 @@ Three main props lets you configure the core features of the `<Admin>` component

Here are all the props accepted by the component:

| Prop | Required | Type | Default | Description |
|------------------- |----------|----------------|----------------|----------------------------------------------------------|
| `dataProvider` | Required | `DataProvider` | - | The data provider for fetching resources |
| `children` | Required | `ReactNode` | - | The routes to render |
| `authCallbackPage` | Optional | `Component` | `AuthCallback` | The content of the authentication callback page |
| `authProvider` | Optional | `AuthProvider` | - | The authentication provider for security and permissions |
| `basename` | Optional | `string` | - | The base path for all URLs |
| `catchAll` | Optional | `Component` | `NotFound` | The fallback component for unknown routes |
| `dashboard` | Optional | `Component` | - | The content of the dashboard page |
| `darkTheme` | Optional | `object` | `default DarkTheme` | The dark theme configuration |
| `defaultTheme` | Optional | `boolean` | `false` | Flag to default to the light theme |
| `disableTelemetry` | Optional | `boolean` | `false` | Set to `true` to disable telemetry collection |
| `i18nProvider` | Optional | `I18NProvider` | - | The internationalization provider for translations |
| `layout` | Optional | `Component` | `Layout` | The content of the layout |
| `loginPage` | Optional | `Component` | `LoginPage` | The content of the login page |
| `notification` | Optional | `Component` | `Notification` | The notification component |
| `queryClient` | Optional | `QueryClient` | - | The react-query client |
| `ready` | Optional | `Component` | `Ready` | The content of the ready page |
| `requireAuth` | Optional | `boolean` | `false` | Flag to require authentication for all routes |
| `store` | Optional | `Store` | - | The Store for managing user preferences |
| `theme` | Optional | `object` | `default LightTheme` | The main (light) theme configuration |
| `title` | Optional | `string` | - | The error page title |
| Prop | Required | Type | Default | Description |
|------------------- |----------|---------------- |--------------------- |---------------------------------------------------------------- |
| `dataProvider` | Required | `DataProvider` | - | The data provider for fetching resources |
| `children` | Required | `ReactNode` | - | The routes to render |
| `authCallbackPage` | Optional | `Component` | `AuthCallback` | The content of the authentication callback page |
| `authProvider` | Optional | `AuthProvider` | - | The authentication provider for security and permissions |
| `basename` | Optional | `string` | - | The base path for all URLs |
| `catchAll` | Optional | `Component` | `NotFound` | The fallback component for unknown routes |
| `dashboard` | Optional | `Component` | - | The content of the dashboard page |
| `darkTheme` | Optional | `object` | `default DarkTheme` | The dark theme configuration |
| `defaultTheme` | Optional | `boolean` | `false` | Flag to default to the light theme |
| `disableTelemetry` | Optional | `boolean` | `false` | Set to `true` to disable telemetry collection |
| `error` | Optional | `Component` | - | A React component rendered in the content area in case of error |
| `i18nProvider` | Optional | `I18NProvider` | - | The internationalization provider for translations |
| `layout` | Optional | `Component` | `Layout` | The content of the layout |
| `loginPage` | Optional | `Component` | `LoginPage` | The content of the login page |
| `notification` | Optional | `Component` | `Notification` | The notification component |
| `queryClient` | Optional | `QueryClient` | - | The react-query client |
| `ready` | Optional | `Component` | `Ready` | The content of the ready page |
| `requireAuth` | Optional | `boolean` | `false` | Flag to require authentication for all routes |
| `store` | Optional | `Store` | - | The Store for managing user preferences |
| `theme` | Optional | `object` | `default LightTheme` | The main (light) theme configuration |
| `title` | Optional | `string` | - | The error page title |


## `dataProvider`
Expand Down Expand Up @@ -521,6 +522,64 @@ const App = () => (
```


## `error`

React-admin uses [React's Error Boundaries](https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary) to render a user-friendly error page in case of client-side JavaScript error, using an internal component called `<Error>`. In production mode, it only displays a generic error message. In development mode, this error page contains the error message and stack trace.

![Default error page](./img/adminError.png)

If you want to customize this error page (e.g. to log the error in a monitoring service), create your own error component, set it as the `<Admin error>` prop, as follows:

```jsx
// in src/App.js
import { Admin } from 'react-admin';
import { MyError } from './MyError';

export const MyLayout = ({ children }) => (
<Admin error={MyError}>
{children}
</Admin>
);
```

React-admin relies on [the `react-error-boundary` package](https://github.com/bvaughn/react-error-boundary) for handling error boundaries. So your custom error component will receive the error, the error info, and a `resetErrorBoundary` function as props. You should call `resetErrorBoundary` upon navigation to remove the error screen.

Here is an example of a custom error component:

```jsx
// in src/MyError.js
import Button from '@mui/material/Button';
import { useResetErrorBoundaryOnLocationChange } from 'react-admin';

export const MyError = ({
error,
resetErrorBoundary,
errorInfo,
}) => {
useResetErrorBoundaryOnLocationChange(errorBoundary);

return (
<div>
<h1>Something Went Wrong </h1>
<div>A client error occurred and your request couldn't be completed.</div>
{process.env.NODE_ENV !== 'production' && (
<details>
<h2>{error.message}</h2>
{errorInfo.componentStack}
</details>
)}
<div>
<Button onClick={() => history.go(-1)}>
Back
</Button>
</div>
</div>
);
};
```

**Tip:** React-admin uses the default `<Error>` component as error boundary **twice**: once in `<Admin>` for errors happening in the layout, and once in `<Layout>` for error happening in CRUD views. The reason is that `<Layout>` renders the navigation menu, giving more possibilities to the user after an error. If you want to customize the error page in the entire app, you should also pass your custom error component to the `<Layout error>` prop. See the [Layout error prop](./Layout.md#error) documentation for more details.

## `i18nProvider`

The `i18nProvider` props let you translate the GUI. For instance, to switch the UI to French instead of the default English:
Expand Down
27 changes: 10 additions & 17 deletions docs/Layout.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,11 +138,11 @@ export const MyLayout = ({ children }) => (

## `error`

Whenever a client-side error happens in react-admin, the user sees an error page. React-admin uses [React's Error Boundaries](https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary) to render this page when any component in the page throws an unrecoverable error.
React-admin uses [React's Error Boundaries](https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary) to render a user-friendly error page in case of client-side JavaScript error, using an internal component called `<Error>`. In production mode, it only displays a generic error message. In development mode, this error page contains the error message and stack trace.

![Default error page](./img/error.webp)

If you want to customize this page, or log the error to a third-party service, create your own `<Error>` component, and pass it to a custom Layout, as follows:
If you want to customize this error page (e.g. to log the error in a monitoring service), create your own error component, and pass it to a custom Layout, as follows:

```jsx
// in src/MyLayout.js
Expand All @@ -156,31 +156,24 @@ export const MyLayout = ({ children }) => (
);
```

The following snippet is a simplified version of the react-admin `Error` component, that you can use as a base for your own:
React-admin relies on [the `react-error-boundary` package](https://github.com/bvaughn/react-error-boundary) for handling error boundaries. So your custom error component will receive the error, the error info, and a `resetErrorBoundary` function as props. You should call `resetErrorBoundary` upon navigation to remove the error screen.

Here is an example of a custom error component:


```jsx
// in src/MyError.js
import * as React from 'react';
import Button from '@mui/material/Button';
import ErrorIcon from '@mui/icons-material/Report';
import History from '@mui/icons-material/History';
import { Title, useTranslate, useDefaultTitle } from 'react-admin';
import { useLocation } from 'react-router-dom';
import { Title, useTranslate, useDefaultTitle, useResetErrorBoundaryOnLocationChange } from 'react-admin';

export const MyError = ({
error,
resetErrorBoundary,
...rest
}) => {
const { pathname } = useLocation();
const originalPathname = useRef(pathname);

// Effect that resets the error state whenever the location changes
useEffect(() => {
if (pathname !== originalPathname.current) {
resetErrorBoundary();
}
}, [pathname, resetErrorBoundary]);
useResetErrorBoundaryOnLocationChange(resetErrorBoundary);

const translate = useTranslate();
const defaultTitle = useDefaultTitle();
Expand All @@ -191,7 +184,7 @@ export const MyError = ({
<div>A client error occurred and your request couldn't be completed.</div>
{process.env.NODE_ENV !== 'production' && (
<details>
<h2>{translate(error.toString())}</h2>
<h2>{translate(error.message)}</h2>
{errorInfo.componentStack}
</details>
)}
Expand All @@ -209,7 +202,7 @@ export const MyError = ({
};
```

**Tip:** [React's Error Boundaries](https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary) are used internally to display the Error Page whenever an error occurs. Error Boundaries only catch errors during rendering, in lifecycle methods, and in constructors of the components tree. This implies in particular that errors during event callbacks (such as 'onClick') are not concerned. Also note that the Error Boundary component is only set around the main container of React Admin. In particular, you won't see it for errors thrown by the [sidebar Menu](./Menu.md), nor the [AppBar](#adding-a-custom-context). This ensures the user is always able to navigate away from the Error Page.
**Tip:** React-admin uses the default `<Error>` component as error boundary **twice**: once in `<Layout>` for error happening in CRUD views, and once in `<Admin>` for errors happening in the layout. If you want to customize the error page in the entire app, you should also pass your custom error component to the `<Admin error>` prop. See the [Admin error prop](./Admin.md#error) documentation for more details.

## `menu`

Expand Down
Binary file added docs/img/adminError.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
41 changes: 41 additions & 0 deletions packages/ra-core/src/core/CoreAdmin.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import * as React from 'react';
import { Route } from 'react-router';
import { CoreAdmin } from './CoreAdmin';
import { CustomRoutes } from './CustomRoutes';

export default {
title: 'ra-core/core/CoreAdmin',
};

const BuggyComponent = () => {
throw new Error('Something went wrong...');
};

export const DefaultError = () => (
<CoreAdmin>
<CustomRoutes noLayout>
<Route path="/" element={<BuggyComponent />} />
</CustomRoutes>
</CoreAdmin>
);

const MyError = ({
error,
errorInfo,
}: {
error?: Error;
errorInfo?: React.ErrorInfo;
}) => (
<div style={{ backgroundColor: 'purple', color: 'white', height: '100vh' }}>
<h1>{error?.message}</h1>
<pre>{errorInfo?.componentStack}</pre>
</div>
);

export const CustomError = () => (
<CoreAdmin error={MyError}>
<CustomRoutes noLayout>
<Route path="/" element={<BuggyComponent />} />
</CustomRoutes>
</CoreAdmin>
);
2 changes: 2 additions & 0 deletions packages/ra-core/src/core/CoreAdmin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ export const CoreAdmin = (props: CoreAdminProps) => {
dashboard,
dataProvider,
disableTelemetry,
error,
i18nProvider,
queryClient,
layout,
Expand All @@ -117,6 +118,7 @@ export const CoreAdmin = (props: CoreAdminProps) => {
catchAll={catchAll}
title={title}
loading={loading}
error={error}
loginPage={loginPage}
requireAuth={requireAuth}
ready={ready}
Expand Down
Loading
Loading