Skip to content
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
1 change: 1 addition & 0 deletions packages/element/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"@availity/mui-divider": "workspace:*",
"@availity/mui-drawer": "workspace:*",
"@availity/mui-empty-state": "workspace:*",
"@availity/mui-error-boundary": "workspace:*",
"@availity/mui-event-tracker": "workspace:*",
"@availity/mui-favorites": "workspace:*",
"@availity/mui-feedback": "workspace:*",
Expand Down
6 changes: 5 additions & 1 deletion packages/element/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,10 @@ export type { DrawerProps } from '@availity/mui-drawer';
export { EmptyState, EmptyStateImages, EmptyStateImage } from '@availity/mui-empty-state';
export type { EmptyStateProps, EmptyStateImageProps } from '@availity/mui-empty-state';

// ErrorBoundary
export { ErrorBoundary } from '@availity/mui-error-boundary';
export type { ErrorBoundaryProps, ErrorBoundaryFallbackProps } from '@availity/mui-error-boundary';

// Event Tracker
export { EventTrackerContext, useEventTracker, EventTrackerProvider } from '@availity/mui-event-tracker';
export type { EventTrackerProps } from '@availity/mui-event-tracker';
Expand Down Expand Up @@ -377,7 +381,7 @@ export {
MoneyBillIcon,
CartShoppingIcon,
BuildingIcon,
HospitalIcon
HospitalIcon,
} from '@availity/mui-icon';

// Layout
Expand Down
Empty file.
98 changes: 98 additions & 0 deletions packages/error-boundary/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# @availity/mui-error-boundary

> Availity MUI ErrorBoundary component to be used with @availity/element design system.

[![Version](https://img.shields.io/npm/v/@availity/mui-error-boundary.svg?style=for-the-badge)](https://www.npmjs.com/package/@availity/mui-error-boundary)
[![NPM Downloads](https://img.shields.io/npm/dt/@availity/mui-error-boundary.svg?style=for-the-badge)](https://www.npmjs.com/package/@availity/mui-error-boundary)
[![Dependency Status](https://img.shields.io/librariesio/release/npm/@availity/mui-error-boundary?style=for-the-badge)](https://github.com/Availity/element/blob/main/packages/error-boundary/package.json)

## Documentation

`ErrorBoundary` catches errors thrown while rendering its children and shows an Availity error page instead of a blank screen. It is built on React's [error boundary](https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary) feature and the Element `EmptyState` component.

Live demo and documentation in our [Storybook](https://availity.github.io/element/?path=/docs/components-error-boundary-introduction--docs)

Availity standards for design and usage can be found in the [Availity Design Guide](https://design.availity.com/2e36e50c7)

## Installation

### Import Through @availity/element (Recommended)

#### NPM

```bash
npm install @availity/element
```

#### Yarn

```bash
yarn add @availity/element
```

### Direct Import

#### NPM

_This package has a few peer dependencies. Add `@mui/material` & `@emotion/react` to your project if not already installed._

```bash
npm install @availity/mui-error-boundary
```

#### Yarn

```bash
yarn add @availity/mui-error-boundary
```

### Usage

#### Import through @availity/element

```tsx
import { ErrorBoundary } from '@availity/element';
```

#### Direct import

```tsx
import { ErrorBoundary } from '@availity/mui-error-boundary';
```

#### Wrap your whole app

Put one boundary inside your app container and use `fullPage` so the error page fills the screen.

```tsx
<ThemeProvider>
<ErrorBoundary fullPage onError={(error, info) => logError(error, info.componentStack)}>
<App />
</ErrorBoundary>
</ThemeProvider>
```

#### Wrap smaller sections

A crash is handled by the closest boundary above it, so wrapping a section keeps the rest of the app running.

```tsx
<ErrorBoundary>
<ClaimsTable />
</ErrorBoundary>
```

#### Props

| Prop | Description |
| ----------- | --------------------------------------------------------------------------------------------------------- |
| `fullPage` | Fill the screen (`min-height: 100vh`) and use an `h1` heading. Default is inline. |
| `fallback` | An element, or `({ error, reset }) => node`, to use instead of the default error page. |
| `onError` | `(error, info) => void`. Called once per caught error. Use it to log to your monitoring tool. |
| `onReset` | Called when the boundary resets, from "Try again" or a `resetKeys` change. |
| `resetKeys` | Array of values. When one changes while an error is showing, the boundary resets. Pass the current route. |
| `sx` | Styles for the default error page container. |

#### What it does not catch

Error boundaries only catch errors thrown **while rendering**. They do not catch errors in event handlers, asynchronous code (promises, `setTimeout`), or errors thrown by the boundary itself. Handle those where they happen.
9 changes: 9 additions & 0 deletions packages/error-boundary/introduction.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Markdown, Meta } from '@storybook/addon-docs/blocks';
import ReadMe from './README.md?raw';
import CHANGELOG from './CHANGELOG.md?raw';

<Meta title="Components/ErrorBoundary/Introduction" />

<Markdown>{ReadMe}</Markdown>

<Markdown>{CHANGELOG}</Markdown>
62 changes: 62 additions & 0 deletions packages/error-boundary/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
{
"name": "@availity/mui-error-boundary",
"version": "0.0.0",
"description": "Availity MUI ErrorBoundary Component - part of the @availity/element design system",
"keywords": [
"react",
"typescript",
"availity",
"mui"
],
"homepage": "https://availity.github.io/element/?path=/docs/components-error-boundary-introduction--docs",
"bugs": {
"url": "https://github.com/Availity/element/issues"
},
"repository": {
"type": "git",
"url": "https://github.com/Availity/element.git",
"directory": "packages/error-boundary"
},
"license": "MIT",
"author": "Availity Developers <AVOSS@availity.com>",
"browser": "./dist/index.js",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
},
"./package.json": "./package.json"
},
"scripts": {
"build": "tsup src/index.ts --format esm --dts",
"dev": "tsup src/index.ts --format esm --watch --dts",
"clean": "rm -rf dist",
"clean:nm": "rm -rf node_modules",
"publish": "yarn npm publish --tolerate-republish --access public",
"publish:canary": "yarn npm publish --access public --tag canary"
},
"dependencies": {
"@availity/mui-button": "workspace:^",
"@availity/mui-empty-state": "workspace:^",
"@availity/mui-layout": "workspace:^",
"@availity/mui-typography": "workspace:^"
},
"devDependencies": {
"@mui/material": "^7.3.11",
"react": "19.2.7",
"react-dom": "19.2.7",
"tsup": "^8.5.1",
"typescript": "^6.0.0"
},
"peerDependencies": {
"@mui/material": "^7.0.0",
"react": ">=17.0.0"
},
"publishConfig": {
"access": "public"
},
"sideEffects": false,
"type": "module"
}
38 changes: 38 additions & 0 deletions packages/error-boundary/project.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"name": "mui-error-boundary",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "packages/error-boundary/src",
"projectType": "library",
"tags": [],
"targets": {
"lint": {
"executor": "@nx/eslint:lint",
"options": {
"silent": false,
"fix": false,
"cache": true,
"cacheLocation": "./node_modules/.cache/error-boundary/.eslintcache",
"maxWarnings": -1,
"quiet": false,
"hasTypeAwareRules": true,
"cacheStrategy": "metadata"
}
},
"test": {
"executor": "nx:run-commands",
"options": {
"command": "vitest run packages/error-boundary"
}
},
"version": {
"executor": "@jscutlery/semver:version",
"options": {
"preset": "conventional",
"commitMessageFormat": "chore({projectName}): release version ${version} [skip ci]",
"tagPrefix": "@availity/{projectName}@",
"trackDeps": true,
"skipCommitTypes": ["docs"]
}
}
}
}
1 change: 1 addition & 0 deletions packages/error-boundary/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './lib/ErrorBoundary';
134 changes: 134 additions & 0 deletions packages/error-boundary/src/lib/ErrorBoundary.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
// Each exported component in the package should have its own stories file

import { useState } from 'react';
import type { Meta, StoryObj } from '@storybook/react-vite';
import { Box } from '@availity/mui-layout';
import { Button } from '@availity/mui-button';
import { Typography } from '@availity/mui-typography';
import { ErrorBoundary, ErrorBoundaryProps } from './ErrorBoundary';

/** Throws while rendering when `shouldThrow` is true, simulating a crash inside the app. */
const Crasher = ({ shouldThrow }: { shouldThrow: boolean }) => {
if (shouldThrow) {
throw new Error('Simulated render error');
}
return <Typography>Everything is working.</Typography>;
};

/** Wraps a section of the app and shows an error page if something inside it crashes while rendering,
* instead of leaving the user with a blank screen.
*
* - Put one boundary around your whole app and use `fullPage` so the error page fills the screen.
* - Put additional boundaries around smaller sections (a widget, a table, a modal) so a crash there does not take down the rest of the page. The closest boundary above the crash handles it.
* - Use `onError` to send the error to your logging or monitoring tool.
* - Use `resetKeys` (for example, the current route) so navigating away from a broken page recovers automatically.
*
* _Error boundaries only catch errors thrown while rendering. They do not catch errors in event handlers or in asynchronous code like promises and timers._
*/
const meta: Meta<typeof ErrorBoundary> = {
title: 'Components/ErrorBoundary/ErrorBoundary',
component: ErrorBoundary,
tags: ['autodocs'],
};

export default meta;

export const _ErrorBoundary: StoryObj<typeof ErrorBoundary> = {
render: (args: ErrorBoundaryProps) => (
<ErrorBoundary {...args}>
<Crasher shouldThrow />
</ErrorBoundary>
),
};

/** Use `fullPage` on the boundary that wraps your whole app. The error page fills the screen and the heading is an `h1`. */
export const _FullPage: StoryObj<typeof ErrorBoundary> = {
render: (args: ErrorBoundaryProps) => (
<ErrorBoundary {...args} fullPage>
<Crasher shouldThrow />
</ErrorBoundary>
),
parameters: {
layout: 'fullscreen',
},
};

/** "Try again" clears the error and renders the children again. Here the crash is only simulated once, so the retry succeeds. */
export const _TryAgain: StoryObj<typeof ErrorBoundary> = {
render: (args: ErrorBoundaryProps) => {
const Example = () => {
const [shouldThrow, setShouldThrow] = useState(true);
return (
<ErrorBoundary {...args} onReset={() => setShouldThrow(false)}>
<Crasher shouldThrow={shouldThrow} />
</ErrorBoundary>
);
};
return <Example />;
},
};

/** Wrap smaller sections so a crash stays contained. The header and the working widget below keep running while the broken widget shows its own error page. */
export const _Nested: StoryObj<typeof ErrorBoundary> = {
render: (args: ErrorBoundaryProps) => (
<ErrorBoundary {...args} fullPage>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<Typography variant="h5" component="h1">
Dashboard
</Typography>
<Box sx={{ display: 'grid', gridTemplateColumns: { xs: '1fr', md: '1fr 1fr' }, gap: 2 }}>
<ErrorBoundary>
<Crasher shouldThrow />
</ErrorBoundary>
<ErrorBoundary>
<Crasher shouldThrow={false} />
</ErrorBoundary>
</Box>
</Box>
</ErrorBoundary>
),
};

/** `fallback` accepts an element, or a function that receives the `error` and a `reset` function. */
export const _CustomFallback: StoryObj<typeof ErrorBoundary> = {
render: (args: ErrorBoundaryProps) => (
<ErrorBoundary
{...args}
fallback={({ error, reset }) => (
<Box role="alert" sx={{ display: 'flex', flexDirection: 'column', gap: 2, alignItems: 'flex-start' }}>
<Typography>This widget could not load ({error.message}).</Typography>
<Button color="primary" onClick={reset}>
Retry
</Button>
</Box>
)}
>
<Crasher shouldThrow />
</ErrorBoundary>
),
};

/** Pass `resetKeys` something that changes when the user moves on, like the current route. When a value changes, the boundary resets itself. */
export const _ResetKeys: StoryObj<typeof ErrorBoundary> = {
render: (args: ErrorBoundaryProps) => {
const Example = () => {
const [page, setPage] = useState<'broken' | 'working'>('broken');
return (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<Box sx={{ display: 'flex', gap: 1 }}>
<Button color="secondary" onClick={() => setPage('broken')}>
Go to broken page
</Button>
<Button color="secondary" onClick={() => setPage('working')}>
Go to working page
</Button>
</Box>
<ErrorBoundary {...args} resetKeys={[page]}>
<Crasher shouldThrow={page === 'broken'} />
</ErrorBoundary>
</Box>
);
};
return <Example />;
},
};
Loading
Loading