From 32446979fce6a091a614887945e6e9742ee01a30 Mon Sep 17 00:00:00 2001 From: Sean Collings Date: Thu, 18 Dec 2025 11:02:53 -0700 Subject: [PATCH 1/7] feat: add ErrorBoundary component with tests and stories --- CLAUDE.md | 121 +++++++++ .../ErrorBoundary/ErrorBoundary.component.tsx | 81 ++++++ .../ErrorBoundary/ErrorBoundary.spec.tsx | 253 ++++++++++++++++++ .../ErrorBoundary/ErrorBoundary.stories.tsx | 234 ++++++++++++++++ .../Feedback/ErrorBoundary/index.tsx | 1 + .../atomic-elements/src/components/index.ts | 2 + 6 files changed, 692 insertions(+) create mode 100644 CLAUDE.md create mode 100644 packages/atomic-elements/src/components/Feedback/ErrorBoundary/ErrorBoundary.component.tsx create mode 100644 packages/atomic-elements/src/components/Feedback/ErrorBoundary/ErrorBoundary.spec.tsx create mode 100644 packages/atomic-elements/src/components/Feedback/ErrorBoundary/ErrorBoundary.stories.tsx create mode 100644 packages/atomic-elements/src/components/Feedback/ErrorBoundary/index.tsx diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..51a72f7b --- /dev/null +++ b/CLAUDE.md @@ -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 diff --git a/packages/atomic-elements/src/components/Feedback/ErrorBoundary/ErrorBoundary.component.tsx b/packages/atomic-elements/src/components/Feedback/ErrorBoundary/ErrorBoundary.component.tsx new file mode 100644 index 00000000..ef44b865 --- /dev/null +++ b/packages/atomic-elements/src/components/Feedback/ErrorBoundary/ErrorBoundary.component.tsx @@ -0,0 +1,81 @@ +import { Component, ErrorInfo, ReactNode } from "react"; + +export interface ErrorBoundaryProps { + children: ReactNode; + fallback: ReactNode | ((error: Error, reset: () => void) => ReactNode); + onError?: (error: Error, errorInfo: ErrorInfo) => void; + onReset?: () => void; + resetKeys?: Array; +} + +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 { + 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; + } +} + +export default ErrorBoundary; diff --git a/packages/atomic-elements/src/components/Feedback/ErrorBoundary/ErrorBoundary.spec.tsx b/packages/atomic-elements/src/components/Feedback/ErrorBoundary/ErrorBoundary.spec.tsx new file mode 100644 index 00000000..b0ea1ec1 --- /dev/null +++ b/packages/atomic-elements/src/components/Feedback/ErrorBoundary/ErrorBoundary.spec.tsx @@ -0,0 +1,253 @@ +import React from "react"; +import { render, screen, fireEvent } from "@testing-library/react"; +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import { ErrorBoundary } from "./ErrorBoundary.component"; + +const ThrowError = ({ shouldThrow }: { shouldThrow: boolean }) => { + if (shouldThrow) { + throw new Error("Test error"); + } + return
No error
; +}; + +describe("ErrorBoundary", () => { + // Suppress console.error for these tests + const originalError = console.error; + beforeEach(() => { + console.error = vi.fn(); + }); + + afterEach(() => { + console.error = originalError; + }); + + it("renders children when there is no error", () => { + render( + Error occurred}> + + + ); + + expect(screen.getByText("No error")).toBeInTheDocument(); + }); + + it("renders fallback when there is an error", () => { + render( + Error occurred}> + + + ); + + expect(screen.getByText("Error occurred")).toBeInTheDocument(); + }); + + it("calls onError when an error occurs", () => { + const onError = vi.fn(); + + render( + Error occurred} onError={onError}> + + + ); + + expect(onError).toHaveBeenCalledWith( + expect.any(Error), + expect.objectContaining({ + componentStack: expect.any(String), + }) + ); + }); + + it("renders function fallback with error and reset", () => { + const fallback = vi.fn((error, reset) => ( +
+ Function fallback + +
+ )); + + render( + + + + ); + + expect(screen.getByText("Function fallback")).toBeInTheDocument(); + expect(fallback).toHaveBeenCalledWith( + expect.any(Error), + expect.any(Function) + ); + }); + + it("resets when resetKeys change", () => { + const onReset = vi.fn(); + const { rerender } = render( + Error occurred} + resetKeys={[1]} + onReset={onReset} + > + + + ); + + expect(screen.getByText("Error occurred")).toBeInTheDocument(); + + rerender( + Error occurred} + resetKeys={[2]} + onReset={onReset} + > + + + ); + + expect(screen.getByText("No error")).toBeInTheDocument(); + expect(onReset).toHaveBeenCalled(); + }); + + it("calls onReset when reset function is called", () => { + const onReset = vi.fn(); + + render( + ( + + )} + onReset={onReset} + > + + + ); + + const resetButton = screen.getByText("Reset"); + fireEvent.click(resetButton); + + expect(onReset).toHaveBeenCalled(); + }); + + it("actually resets the error boundary when reset is called", () => { + const TestComponent = () => { + const [shouldThrow, setShouldThrow] = React.useState(true); + + return ( + ( +
+ Error: {error.message} + +
+ )} + > + +
+ ); + }; + + render(); + + // Should show error initially + expect(screen.getByText("Error: Test error")).toBeInTheDocument(); + + // Click reset + fireEvent.click(screen.getByText("Reset and Fix")); + + // Should now show the working component + expect(screen.getByText("No error")).toBeInTheDocument(); + }); + + it("handles resetKeys array changes correctly", () => { + const onReset = vi.fn(); + const { rerender } = render( + Error occurred} + resetKeys={["key1", "key2"]} + onReset={onReset} + > + + + ); + + expect(screen.getByText("Error occurred")).toBeInTheDocument(); + + // Change one key in the array + rerender( + Error occurred} + resetKeys={["key1", "key3"]} + onReset={onReset} + > + + + ); + + expect(screen.getByText("No error")).toBeInTheDocument(); + expect(onReset).toHaveBeenCalled(); + }); + + it("handles empty resetKeys properly", () => { + const onReset = vi.fn(); + const { rerender } = render( + Error occurred} + resetKeys={[]} + onReset={onReset} + > + + + ); + + expect(screen.getByText("Error occurred")).toBeInTheDocument(); + + // Change from empty to populated + rerender( + Error occurred} + resetKeys={["key1"]} + onReset={onReset} + > + + + ); + + expect(screen.getByText("No error")).toBeInTheDocument(); + expect(onReset).toHaveBeenCalled(); + }); + + it("does not reset when resetKeys array is the same", () => { + const onReset = vi.fn(); + const { rerender } = render( + Error occurred} + resetKeys={["key1", "key2"]} + onReset={onReset} + > + + + ); + + expect(screen.getByText("Error occurred")).toBeInTheDocument(); + + // Rerender with same keys + rerender( + Error occurred} + resetKeys={["key1", "key2"]} + onReset={onReset} + > + + + ); + + expect(screen.getByText("Error occurred")).toBeInTheDocument(); + expect(onReset).not.toHaveBeenCalled(); + }); +}); \ No newline at end of file diff --git a/packages/atomic-elements/src/components/Feedback/ErrorBoundary/ErrorBoundary.stories.tsx b/packages/atomic-elements/src/components/Feedback/ErrorBoundary/ErrorBoundary.stories.tsx new file mode 100644 index 00000000..39ca68b0 --- /dev/null +++ b/packages/atomic-elements/src/components/Feedback/ErrorBoundary/ErrorBoundary.stories.tsx @@ -0,0 +1,234 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import React, { useState } from "react"; +import { ErrorBoundary } from "./ErrorBoundary.component"; +import { Button } from "../../Buttons/Button"; +import { Banner } from "../../Banners/Banner"; +import { Text } from "@components/Typography/Text"; + +const meta: Meta = { + title: "Feedback/ErrorBoundary", + component: ErrorBoundary, + parameters: { + layout: "centered", + }, +}; + +export default meta; +type Story = StoryObj; + +const BuggyComponent = ({ shouldThrow }: { shouldThrow: boolean }) => { + if (shouldThrow) { + throw new Error("Something went wrong in the component!"); + } + return ( + + Everything is working fine! + + ); +}; + +const BuggyCounter = () => { + const [count, setCount] = useState(0); + + if (count > 3) { + throw new Error("Count cannot be greater than 3!"); + } + + return ( +
+

Count: {count}

+ +
+ ); +}; + +export const WithStaticFallback: Story = { + render: () => { + const [shouldThrow, setShouldThrow] = useState(false); + + return ( +
+ + + + + Oops! Something went wrong. Please try refreshing the page. + + + } + > + + +
+ ); + }, +}; + +export const WithFunctionFallback: Story = { + render: () => { + const [shouldThrow, setShouldThrow] = useState(false); + + return ( +
+ + + ( + + + Error: {error.message} +
+ +
+
+ )} + resetKeys={[shouldThrow]} + onError={(error, errorInfo) => { + console.log("Error caught:", error); + console.log("Error info:", errorInfo); + }} + onReset={() => { + console.log("Error boundary reset"); + }} + > + +
+
+ ); + }, +}; + +export const WithResetKeys: Story = { + render: () => { + const [resetKey, setResetKey] = useState(0); + const [userId, setUserId] = useState("user1"); + + return ( +
+
+

+ Reset key: {resetKey} | User ID: {userId} +

+
+ + +
+

+ Changing either value will reset the error boundary +

+
+ + + + Component crashed! Change the reset key or user ID above to + recover. + + + } + resetKeys={[resetKey, userId]} + onReset={() => { + console.log("Error boundary reset due to resetKeys change"); + }} + > + + +
+ ); + }, +}; + +export const NestedErrorBoundaries: Story = { + render: () => { + const [outerError, setOuterError] = useState(false); + const [innerError, setInnerError] = useState(false); + + return ( +
+
+ + +
+ + + + Outer error boundary caught an error! + + + } + > +
+

Outer Component

+ + + + + Inner error boundary caught an error! + + + } + resetKeys={[innerError]} + > +
+

Inner Component

+ +
+
+
+
+
+ ); + }, +}; diff --git a/packages/atomic-elements/src/components/Feedback/ErrorBoundary/index.tsx b/packages/atomic-elements/src/components/Feedback/ErrorBoundary/index.tsx new file mode 100644 index 00000000..cc723b74 --- /dev/null +++ b/packages/atomic-elements/src/components/Feedback/ErrorBoundary/index.tsx @@ -0,0 +1 @@ +export { ErrorBoundary, type ErrorBoundaryProps } from "./ErrorBoundary.component"; \ No newline at end of file diff --git a/packages/atomic-elements/src/components/index.ts b/packages/atomic-elements/src/components/index.ts index 5634b170..a196aeec 100644 --- a/packages/atomic-elements/src/components/index.ts +++ b/packages/atomic-elements/src/components/index.ts @@ -51,6 +51,7 @@ export { ThreeDotLoader } from "./Feedback/ThreeDotLoader"; export { SkeletonLoader } from "./Feedback/SkeletonLoader"; export { LoadingStatus } from "./Feedback/LoadingStatus"; export { ProgressCircle } from "./Feedback/ProgressCircle"; +export { ErrorBoundary } from "./Feedback/ErrorBoundary"; export { MaterialIcon } from "./Icons/MaterialIcon"; export { MaterialSymbol } from "./Icons/MaterialSymbol"; @@ -162,6 +163,7 @@ export type { ThreeDotLoaderProps } from "./Feedback/ThreeDotLoader"; export type { SkeletonLoaderProps } from "./Feedback/SkeletonLoader"; export type { LoadingStatusProps } from "./Feedback/LoadingStatus"; export type { ProgressCircleProps } from "./Feedback/ProgressCircle"; +export type { ErrorBoundaryProps } from "./Feedback/ErrorBoundary"; export type { ModalProps } from "./Overlays/Modal"; export type { ConfirmationModalProps } from "./Overlays/ConfirmationModal"; export type { ErrorModalProps } from "./Overlays/ErrorModal"; From 34b8a6b11728062a48ddb5668b780521fd935db1 Mon Sep 17 00:00:00 2001 From: Sean Collings Date: Thu, 18 Dec 2025 11:20:54 -0700 Subject: [PATCH 2/7] version: update --- packages/atomic-elements/CHANGELOG.md | 6 ++++++ packages/atomic-elements/package.json | 2 +- packages/forms/CHANGELOG.md | 7 +++++++ packages/forms/package.json | 6 +++--- 4 files changed, 17 insertions(+), 4 deletions(-) diff --git a/packages/atomic-elements/CHANGELOG.md b/packages/atomic-elements/CHANGELOG.md index e8fbd29d..840c109f 100644 --- a/packages/atomic-elements/CHANGELOG.md +++ b/packages/atomic-elements/CHANGELOG.md @@ -1,5 +1,11 @@ # @atomicjolt/atomic-elements +## 3.6.0 + +### Minor Changes + +- Add ErrorBoundary helper component + ## 3.5.1 ### Patch Changes diff --git a/packages/atomic-elements/package.json b/packages/atomic-elements/package.json index f8e06fe3..666fefa0 100644 --- a/packages/atomic-elements/package.json +++ b/packages/atomic-elements/package.json @@ -1,6 +1,6 @@ { "name": "@atomicjolt/atomic-elements", - "version": "3.5.1", + "version": "3.6.0", "sideEffects": false, "module": "dist/esm/index.js", "main": "dist/cjs/index.js", diff --git a/packages/forms/CHANGELOG.md b/packages/forms/CHANGELOG.md index 7d340d4a..de8331cb 100644 --- a/packages/forms/CHANGELOG.md +++ b/packages/forms/CHANGELOG.md @@ -1,5 +1,12 @@ # @atomicjolt/forms +## 4.0.0 + +### Patch Changes + +- Updated dependencies + - @atomicjolt/atomic-elements@3.6.0 + ## 3.5.0 ### Patch Changes diff --git a/packages/forms/package.json b/packages/forms/package.json index c70cf425..55d9d0c5 100644 --- a/packages/forms/package.json +++ b/packages/forms/package.json @@ -1,6 +1,6 @@ { "name": "@atomicjolt/forms", - "version": "3.5.0", + "version": "4.0.0", "license": "MIT", "type": "module", "main": "dist/index.js", @@ -17,13 +17,13 @@ "prepublishOnly": "npm run build" }, "peerDependencies": { - "@atomicjolt/atomic-elements": "^3.5.0", + "@atomicjolt/atomic-elements": "^3.6.0", "react": "^18 || ^19", "react-dom": "^18 || ^19", "react-hook-form": "^7.47.0" }, "devDependencies": { - "@atomicjolt/atomic-elements": "^3.5.0", + "@atomicjolt/atomic-elements": "^3.6.0", "@types/node": "^20.8.4", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", From 07b00fdfec4e5077934fe0c20f3d6328538950f3 Mon Sep 17 00:00:00 2001 From: Sean Collings Date: Thu, 18 Dec 2025 11:33:00 -0700 Subject: [PATCH 3/7] chore: update argTypes for ErrorBoundary --- .../ErrorBoundary/ErrorBoundary.component.tsx | 4 ++++ .../ErrorBoundary/ErrorBoundary.stories.tsx | 20 ++++++++++++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/packages/atomic-elements/src/components/Feedback/ErrorBoundary/ErrorBoundary.component.tsx b/packages/atomic-elements/src/components/Feedback/ErrorBoundary/ErrorBoundary.component.tsx index ef44b865..3ebe5d0e 100644 --- a/packages/atomic-elements/src/components/Feedback/ErrorBoundary/ErrorBoundary.component.tsx +++ b/packages/atomic-elements/src/components/Feedback/ErrorBoundary/ErrorBoundary.component.tsx @@ -2,9 +2,13 @@ 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; } diff --git a/packages/atomic-elements/src/components/Feedback/ErrorBoundary/ErrorBoundary.stories.tsx b/packages/atomic-elements/src/components/Feedback/ErrorBoundary/ErrorBoundary.stories.tsx index 39ca68b0..806f343b 100644 --- a/packages/atomic-elements/src/components/Feedback/ErrorBoundary/ErrorBoundary.stories.tsx +++ b/packages/atomic-elements/src/components/Feedback/ErrorBoundary/ErrorBoundary.stories.tsx @@ -1,5 +1,5 @@ +import { useState } from "react"; import type { Meta, StoryObj } from "@storybook/react"; -import React, { useState } from "react"; import { ErrorBoundary } from "./ErrorBoundary.component"; import { Button } from "../../Buttons/Button"; import { Banner } from "../../Banners/Banner"; @@ -11,6 +11,24 @@ const meta: Meta = { parameters: { layout: "centered", }, + argTypes: { + fallback: { + control: "text", + description: `The fallback UI to display when an error is caught. This can be a React node or a function that receives the error and a reset function as arguments.`, + }, + resetKeys: { + control: false, + description: `An array of values that, when changed, will reset the error boundary's state.`, + }, + onError: { + action: "error captured", + description: `Callback function that is called when an error is caught. It receives the error and additional info as arguments.`, + }, + onReset: { + action: "reset triggered", + description: `Callback function that is called when the error boundary is reset.`, + }, + }, }; export default meta; From 40e4a86acba24d080243c2fb1545f148e203dfc8 Mon Sep 17 00:00:00 2001 From: Sean Collings Date: Thu, 18 Dec 2025 11:38:30 -0700 Subject: [PATCH 4/7] fix: fix version in @atomicjolt/forms --- packages/forms/CHANGELOG.md | 2 +- packages/forms/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/forms/CHANGELOG.md b/packages/forms/CHANGELOG.md index de8331cb..3505bf65 100644 --- a/packages/forms/CHANGELOG.md +++ b/packages/forms/CHANGELOG.md @@ -1,6 +1,6 @@ # @atomicjolt/forms -## 4.0.0 +## 3.6.0 ### Patch Changes diff --git a/packages/forms/package.json b/packages/forms/package.json index 55d9d0c5..3b0f2220 100644 --- a/packages/forms/package.json +++ b/packages/forms/package.json @@ -1,6 +1,6 @@ { "name": "@atomicjolt/forms", - "version": "4.0.0", + "version": "3.6.0", "license": "MIT", "type": "module", "main": "dist/index.js", From 0dd95dea11272ed49c0c6afd9e4a7c21caeb879c Mon Sep 17 00:00:00 2001 From: Sean Collings Date: Thu, 18 Dec 2025 11:44:39 -0700 Subject: [PATCH 5/7] fix: remove default export --- .../Feedback/ErrorBoundary/ErrorBoundary.component.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/atomic-elements/src/components/Feedback/ErrorBoundary/ErrorBoundary.component.tsx b/packages/atomic-elements/src/components/Feedback/ErrorBoundary/ErrorBoundary.component.tsx index 3ebe5d0e..508cc51e 100644 --- a/packages/atomic-elements/src/components/Feedback/ErrorBoundary/ErrorBoundary.component.tsx +++ b/packages/atomic-elements/src/components/Feedback/ErrorBoundary/ErrorBoundary.component.tsx @@ -81,5 +81,3 @@ export class ErrorBoundary extends Component< return this.props.children; } } - -export default ErrorBoundary; From 9b37d503cddae2daf54eeb54947f1056786127cd Mon Sep 17 00:00:00 2001 From: Sean Collings Date: Thu, 18 Dec 2025 11:52:21 -0700 Subject: [PATCH 6/7] docs: switch from Banner to ErrorBanner --- .../ErrorBoundary/ErrorBoundary.stories.tsx | 62 +++++++------------ 1 file changed, 24 insertions(+), 38 deletions(-) diff --git a/packages/atomic-elements/src/components/Feedback/ErrorBoundary/ErrorBoundary.stories.tsx b/packages/atomic-elements/src/components/Feedback/ErrorBoundary/ErrorBoundary.stories.tsx index 806f343b..3e218a39 100644 --- a/packages/atomic-elements/src/components/Feedback/ErrorBoundary/ErrorBoundary.stories.tsx +++ b/packages/atomic-elements/src/components/Feedback/ErrorBoundary/ErrorBoundary.stories.tsx @@ -2,7 +2,7 @@ import { useState } from "react"; import type { Meta, StoryObj } from "@storybook/react"; import { ErrorBoundary } from "./ErrorBoundary.component"; import { Button } from "../../Buttons/Button"; -import { Banner } from "../../Banners/Banner"; +import { ErrorBanner } from "@components/Banners/DismissableBanner"; import { Text } from "@components/Typography/Text"; const meta: Meta = { @@ -76,11 +76,9 @@ export const WithStaticFallback: Story = { - - Oops! Something went wrong. Please try refreshing the page. - - + + Oops! Something went wrong. Please try refreshing the page. + } > @@ -105,22 +103,20 @@ export const WithFunctionFallback: Story = { ( - - - Error: {error.message} -
- -
-
+ + Error: {error.message} +
+ +
)} resetKeys={[shouldThrow]} onError={(error, errorInfo) => { @@ -166,12 +162,10 @@ export const WithResetKeys: Story = { - - Component crashed! Change the reset key or user ID above to - recover. - - + + Component crashed! Change the reset key or user ID above to + recover. + } resetKeys={[resetKey, userId]} onReset={() => { @@ -210,11 +204,7 @@ export const NestedErrorBoundaries: Story = { - - Outer error boundary caught an error! - - + Outer error boundary caught an error! } >
- - Inner error boundary caught an error! - - + Inner error boundary caught an error! } resetKeys={[innerError]} > From 0bbae6e7c88f3cc3c30504ca6c4986eb064d4281 Mon Sep 17 00:00:00 2001 From: Sean Collings Date: Thu, 18 Dec 2025 12:02:56 -0700 Subject: [PATCH 7/7] docs: add ErrorBoundary documentation with usage examples and best practices --- .../docs/Guides/ErrorBoundary.mdx | 159 ++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 packages/atomic-elements/docs/Guides/ErrorBoundary.mdx diff --git a/packages/atomic-elements/docs/Guides/ErrorBoundary.mdx b/packages/atomic-elements/docs/Guides/ErrorBoundary.mdx new file mode 100644 index 00000000..cfc29fc7 --- /dev/null +++ b/packages/atomic-elements/docs/Guides/ErrorBoundary.mdx @@ -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 ( + Something went wrong!
}> + +
+ ); +} +``` + +### Function Fallback + +For more dynamic error handling, use a function fallback: + +```tsx +function App() { + return ( + ( +
+

Oops! Something went wrong

+

Error: {error.message}

+ +
+ )} + > + +
+ ); +} +``` + +## 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 ( + ( + +

Failed to load dashboard: {error.message}

+ +
+ )} + onReset={() => { + console.log("Dashboard error boundary reset"); + }} + > + +
+ ); +} +``` + +## 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 ( + ( +
+

Component Error

+

{error.message}

+ +
+ )} + > + +
+ ); +} +``` + +## Error Monitoring + +Use the `onError` callback to report errors to monitoring services: + +```tsx +function MonitoredApp() { + return ( + { + // Report to your error monitoring service + errorReporting.captureException(error, { + extra: errorInfo, + tags: { component: "App" }, + }); + }} + fallback={(error, reset) => ( + +

Something went wrong

+

We've been notified and are working on a fix.

+ +
+ )} + > + +
+ ); +} +``` + +## 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