-
Notifications
You must be signed in to change notification settings - Fork 1
Error Boundary #213
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
Merged
Merged
Error Boundary #213
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
3244697
feat: add ErrorBoundary component with tests and stories
seanrcollings 34b8a6b
version: update
seanrcollings 07b00fd
chore: update argTypes for ErrorBoundary
seanrcollings 40e4a86
fix: fix version in @atomicjolt/forms
seanrcollings 0dd95de
fix: remove default export
seanrcollings 9b37d50
docs: switch from Banner to ErrorBanner
seanrcollings 0bbae6e
docs: add ErrorBoundary documentation with usage examples and best pr…
seanrcollings File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,121 @@ | ||
| # CLAUDE.md | ||
|
|
||
| This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. | ||
|
|
||
| ## Development Commands | ||
|
|
||
| ### Build and Testing | ||
| ```bash | ||
| # Install dependencies | ||
| npm install | ||
|
|
||
| # Build all packages | ||
| npm run build --workspaces | ||
|
|
||
| # Test all packages (requires build first) | ||
| npm run test --workspaces | ||
|
|
||
| # Test single package | ||
| npm run test --workspace @atomicjolt/atomic-elements | ||
|
|
||
| # Watch tests for a specific package | ||
| npm run test:watch --workspace @atomicjolt/atomic-elements | ||
|
|
||
| # Update test snapshots | ||
| npm run test:update --workspace @atomicjolt/atomic-elements | ||
| ``` | ||
|
|
||
| ### Development Environment | ||
| ```bash | ||
| # Run the interactive playground | ||
| npm run playground | ||
|
|
||
| # Start Storybook development server | ||
| npm run storybook:dev | ||
|
|
||
| # Start documentation development server | ||
| npm run docs:start | ||
| ``` | ||
|
|
||
| ### Version Management (Changesets) | ||
| ```bash | ||
| # Create a new changeset for package changes | ||
| npx changeset | ||
|
|
||
| # Check changeset status | ||
| npx changeset status | ||
|
|
||
| # Version packages based on changesets | ||
| npx changeset version | ||
|
|
||
| # Publish new package versions | ||
| npx changeset publish | ||
| ``` | ||
|
|
||
| ### Component and Package Scaffolding | ||
| ```bash | ||
| # Create new component in atomic-elements | ||
| npm run scaffold:component | ||
|
|
||
| # Create new package | ||
| npm run scaffold:package | ||
| ``` | ||
|
|
||
| ## Repository Architecture | ||
|
|
||
| This is a monorepo containing multiple NPM packages maintained by Atomic Jolt, built with: | ||
| - **Workspaces**: npm workspaces for monorepo management | ||
| - **Build tools**: Rollup (packages), Vite (dev/playground), TypeScript | ||
| - **Testing**: Vitest with jsdom environment | ||
| - **Documentation**: Storybook + Docusaurus | ||
| - **Components**: React with styled-components, React Aria, React Stately | ||
|
|
||
| ### Package Structure | ||
|
|
||
| **Main packages** (all in `packages/` directory): | ||
| - `atomic-elements` - Core React component library with design system | ||
| - `atomic-fuel` - Redux utilities and React components for LTI applications | ||
| - `hooks` - Reusable React hooks | ||
| - `forms` - Form components and utilities | ||
| - `lti-client` - LTI client-side utilities and components | ||
| - `lti-server` - LTI server-side utilities and validation | ||
| - `lti-components` - LTI-specific React components | ||
| - `lti-types` - TypeScript types for LTI | ||
| - `canvas-client` - Canvas API client utilities | ||
|
|
||
| ### Key Directories | ||
|
|
||
| - `packages/atomic-elements/src/components/` - All UI components organized by category (Buttons, Fields, Layout, etc.) | ||
| - `packages/atomic-elements/src/styles/` - Design system tokens, themes, and styling utilities | ||
| - `packages/atomic-elements/src/hooks/` - Component-specific hooks | ||
| - `.storybook/` - Storybook configuration and utilities | ||
| - `playground/` - Interactive development environment | ||
| - `templates/` - Scaffolding templates for new components and packages | ||
| - `docs/` - Documentation source files (Docusaurus) | ||
|
|
||
| ### Component Architecture | ||
|
|
||
| Components follow a consistent structure: | ||
| - `ComponentName.component.tsx` - Main component implementation | ||
| - `ComponentName.styles.ts` - Styled-components styling | ||
| - `ComponentName.context.ts` - React context (if needed) | ||
| - `ComponentName.stories.tsx` - Storybook stories | ||
| - `ComponentName.spec.tsx` - Vitest tests | ||
| - `ComponentName.types.ts` - TypeScript types (if complex) | ||
| - `index.tsx` - Public exports | ||
|
|
||
| ### Build System | ||
|
|
||
| - Each package has its own `tsconfig.build.json` and build configuration | ||
| - `atomic-elements` uses Rollup to generate both ESM and CJS outputs | ||
| - Path aliases configured in `vite.config.ts` for easier imports (@components, @hooks, @styles, @utils) | ||
| - TypeScript with strict mode enabled | ||
| - Components built with React 18/19 compatibility | ||
|
|
||
| ### Testing Strategy | ||
|
|
||
| - Vitest for all testing with jsdom environment | ||
| - React Testing Library for component testing | ||
| - Each package has individual test configuration | ||
| - Shared test setup in `vitest.setup.ts` files | ||
| - Build packages before running tests to ensure proper imports |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,159 @@ | ||
| --- | ||
| title: ErrorBoundary | ||
| --- | ||
|
|
||
| import { ErrorBoundary } from "@atomicjolt/atomic-elements"; | ||
|
|
||
| # ErrorBoundary | ||
|
|
||
| The `ErrorBoundary` component provides a way to catch JavaScript errors in your component tree and display fallback UI instead of crashing the entire application. | ||
|
|
||
| ## Basic Usage | ||
|
|
||
| ### Static Fallback | ||
|
|
||
| The simplest usage is with a static fallback UI: | ||
|
|
||
| ```tsx | ||
| import { ErrorBoundary } from "@atomicjolt/atomic-elements"; | ||
|
|
||
| function App() { | ||
| return ( | ||
| <ErrorBoundary fallback={<div>Something went wrong!</div>}> | ||
| <MyComponent /> | ||
| </ErrorBoundary> | ||
| ); | ||
| } | ||
| ``` | ||
|
|
||
| ### Function Fallback | ||
|
|
||
| For more dynamic error handling, use a function fallback: | ||
|
|
||
| ```tsx | ||
| function App() { | ||
| return ( | ||
| <ErrorBoundary | ||
| fallback={(error, reset) => ( | ||
| <div> | ||
| <h2>Oops! Something went wrong</h2> | ||
| <p>Error: {error.message}</p> | ||
| <button onClick={reset}>Try again</button> | ||
| </div> | ||
| )} | ||
| > | ||
| <MyComponent /> | ||
| </ErrorBoundary> | ||
| ); | ||
| } | ||
| ``` | ||
|
|
||
| ## Reset Keys | ||
|
|
||
| The `resetKeys` prop allows the ErrorBoundary to automatically reset when certain values change. This is useful for resetting the error state when the conditions that caused the error are no longer present. | ||
|
|
||
| When any value in the `resetKeys` array changes (using strict equality `!==` comparison), the ErrorBoundary will: | ||
|
|
||
| 1. Clear the error state | ||
| 2. Call the `onReset` callback (if provided) | ||
| 3. Re-render the children components | ||
|
|
||
| ```tsx | ||
| function UserDashboard() { | ||
| const { currentUser } = useAuth(); | ||
| const [retryCount, setRetryCount] = useState(0); | ||
|
|
||
| return ( | ||
| <ErrorBoundary | ||
| // Reset when user changes or when retryCount increments | ||
| resetKeys={[currentUser?.id, retryCount]} | ||
| fallback={(error, reset) => ( | ||
| <ErrorBanner> | ||
| <p>Failed to load dashboard: {error.message}</p> | ||
| <Button onPress={() => setRetryCount((prev) => prev + 1)}> | ||
| Try Again | ||
| </Button> | ||
| </ErrorBanner> | ||
| )} | ||
| onReset={() => { | ||
| console.log("Dashboard error boundary reset"); | ||
| }} | ||
| > | ||
| <Dashboard user={currentUser} /> | ||
| </ErrorBoundary> | ||
| ); | ||
| } | ||
| ``` | ||
|
|
||
| ## Manual Reset | ||
|
|
||
| You can also trigger a reset manually using the `reset` function provided to function fallbacks: | ||
|
|
||
| ```tsx | ||
| function AppWithManualReset() { | ||
| const [shouldThrow, setShouldThrow] = useState(true); | ||
|
|
||
| return ( | ||
| <ErrorBoundary | ||
| fallback={(error, reset) => ( | ||
| <div> | ||
| <h2>Component Error</h2> | ||
| <p>{error.message}</p> | ||
| <button | ||
| onClick={() => | ||
| // Manually reset the error boundary state | ||
| reset(); | ||
| }} | ||
| > | ||
| Fix and Retry | ||
| </button> | ||
| </div> | ||
| )} | ||
| > | ||
| <BuggyComponent /> | ||
| </ErrorBoundary> | ||
| ); | ||
| } | ||
| ``` | ||
|
|
||
| ## Error Monitoring | ||
|
|
||
| Use the `onError` callback to report errors to monitoring services: | ||
|
|
||
| ```tsx | ||
| function MonitoredApp() { | ||
| return ( | ||
| <ErrorBoundary | ||
| onError={(error, errorInfo) => { | ||
| // Report to your error monitoring service | ||
| errorReporting.captureException(error, { | ||
| extra: errorInfo, | ||
| tags: { component: "App" }, | ||
| }); | ||
| }} | ||
| fallback={(error, reset) => ( | ||
| <ErrorBanner> | ||
| <h3>Something went wrong</h3> | ||
| <p>We've been notified and are working on a fix.</p> | ||
| <Button onPress={reset}>Try Again</Button> | ||
| </ErrorBanner> | ||
| )} | ||
| > | ||
| <App /> | ||
| </ErrorBoundary> | ||
| ); | ||
| } | ||
| ``` | ||
|
|
||
| ## Best Practices | ||
|
|
||
| - **Use resetKeys** - Include any state that could affect whether the error occurs | ||
| - **Provide meaningful fallbacks** - Show what went wrong and how to recover | ||
| - **Report errors** - Use the `onError` callback to send errors to monitoring services | ||
| - **Test your error boundaries** - Throw errors in development to verify your fallbacks work | ||
|
|
||
| ## Limitations | ||
|
|
||
| - Error boundaries only catch errors in child components, not in the error boundary itself | ||
| - Error boundaries don't catch errors in event handlers - use try/catch for those | ||
| - Error boundaries don't catch errors in async code - handle Promise rejections separately |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
83 changes: 83 additions & 0 deletions
83
packages/atomic-elements/src/components/Feedback/ErrorBoundary/ErrorBoundary.component.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,83 @@ | ||
| import { Component, ErrorInfo, ReactNode } from "react"; | ||
|
|
||
| export interface ErrorBoundaryProps { | ||
| children: ReactNode; | ||
| /** Fallback UI to display when an error is caught. Can be a ReactNode or a function that receives the error and a reset function. */ | ||
| fallback: ReactNode | ((error: Error, reset: () => void) => ReactNode); | ||
| /** Callback invoked when an error is caught. */ | ||
| onError?: (error: Error, errorInfo: ErrorInfo) => void; | ||
| /** Callback invoked when the error boundary is reset. */ | ||
| onReset?: () => void; | ||
| /** An array of values that, when changed, will reset the error boundary. */ | ||
| resetKeys?: Array<any>; | ||
| } | ||
|
|
||
| interface ErrorBoundaryState { | ||
| hasError: boolean; | ||
| error: Error | null; | ||
| } | ||
|
|
||
| export class ErrorBoundary extends Component< | ||
| ErrorBoundaryProps, | ||
| ErrorBoundaryState | ||
| > { | ||
| constructor(props: ErrorBoundaryProps) { | ||
| super(props); | ||
| this.state = { | ||
| hasError: false, | ||
| error: null, | ||
| }; | ||
| } | ||
|
|
||
| static getDerivedStateFromError(error: Error): Partial<ErrorBoundaryState> { | ||
| return { | ||
| hasError: true, | ||
| error, | ||
| }; | ||
| } | ||
|
|
||
| componentDidCatch(error: Error, errorInfo: ErrorInfo) { | ||
| this.props.onError?.(error, errorInfo); | ||
| } | ||
|
|
||
| componentDidUpdate(prevProps: ErrorBoundaryProps) { | ||
| const { resetKeys } = this.props; | ||
| const prevResetKeys = prevProps.resetKeys; | ||
|
|
||
| if (this.state.hasError && prevResetKeys !== resetKeys) { | ||
| if (resetKeys && prevResetKeys) { | ||
| const hasResetKeyChanged = resetKeys.some( | ||
| (key, idx) => prevResetKeys[idx] !== key | ||
| ); | ||
| if (hasResetKeyChanged) { | ||
| this.reset(); | ||
| } | ||
| } else if (resetKeys !== prevResetKeys) { | ||
| this.reset(); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| reset = () => { | ||
| this.props.onReset?.(); | ||
|
|
||
| this.setState({ | ||
| hasError: false, | ||
| error: null, | ||
| }); | ||
| }; | ||
|
|
||
| render() { | ||
| if (this.state.hasError && this.state.error) { | ||
| const { fallback } = this.props; | ||
|
|
||
| if (typeof fallback === "function") { | ||
| return fallback(this.state.error, this.reset); | ||
| } | ||
|
|
||
| return fallback; | ||
| } | ||
|
|
||
| return this.props.children; | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.