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 7 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
115 changes: 93 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,76 @@ 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 client-side errors happens in react-admin.

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

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:

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

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

```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 { useLocation } from 'react-router-dom';

export const MyError = ({
error,
resetErrorBoundary,
errorInfo,
}) => {
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]);

return (
<div>
<h1><ErrorIcon /> 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.toString()}</h2>
{errorInfo.componentStack}
</details>
)}
<div>
<Button
variant="contained"
startIcon={<History />}
onClick={() => history.go(-1)}
>
Back
</Button>
</div>
</div>
);
};
```

**Tip:** Some errors appear in the `<Layout>` or in one of their children. To customize this error page too, you can use the [`<Layout>` `error` prop](./Layout.md#error).


## `i18nProvider`

The `i18nProvider` props let you translate the GUI. For instance, to switch the UI to French instead of the default English:
Expand Down
2 changes: 2 additions & 0 deletions docs/Layout.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,8 @@ 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:** Some errors appear in a higher level than the layout. To customize that second error page, you can use the [`<Admin error>` prop](./Admin.md#error).

## `menu`

Lets you override the 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.
40 changes: 40 additions & 0 deletions packages/ra-core/src/core/CoreAdmin.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import * as React from 'react';
import { Route } from 'react-router';
import { CoreAdmin } from './CoreAdmin';
import { CustomRoutes } from './CustomRoutes';
import { FakeBrowserDecorator } from '../storybook/FakeBrowser';

export default {
title: 'ra-core/Admin',
decorators: [FakeBrowserDecorator],
parameters: {
initialEntries: ['/error'],
},
};

const ErrorComponent = () => {
throw Error();
};

export const Error = () => (
<CoreAdmin>
<CustomRoutes noLayout>
<Route path="/error" element={<ErrorComponent />} />
</CustomRoutes>
</CoreAdmin>
);

const MyError = ({ errorInfo }: { errorInfo?: React.ErrorInfo }) => (
<div style={{ backgroundColor: 'purple', color: 'white', height: '100vh' }}>
<h1>Something went wrong...</h1>
<p>{errorInfo?.componentStack}</p>
</div>
);

export const CustomError = () => (
<CoreAdmin error={MyError}>
<CustomRoutes noLayout>
<Route path="/error" element={<ErrorComponent />} />
</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