Repository: bvaughn/react-error-boundary Branch: main Commit: ecaf9257e11c Files: 122 Total size: 129.2 KB Directory structure: gitextract_msysdob0/ ├── .gitattributes ├── .github/ │ └── workflows/ │ ├── eslint.yml │ ├── pending-changes.yml │ ├── prettier.yml │ ├── typescript.yml │ └── vitest.yml ├── .gitignore ├── .nvmrc ├── .prettierignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── eslint.config.js ├── index.css ├── index.html ├── index.tsx ├── integrations/ │ └── vite/ │ ├── README.md │ ├── eslint.config.js │ ├── index.html │ ├── package.json │ ├── playwright.config.ts │ ├── src/ │ │ ├── components/ │ │ │ ├── Children.tsx │ │ │ ├── Container.tsx │ │ │ ├── DebugData.tsx │ │ │ └── Resizer.tsx │ │ ├── index.css │ │ ├── main.tsx │ │ ├── routes/ │ │ │ └── Home.tsx │ │ ├── utils/ │ │ │ ├── assert.ts │ │ │ └── cn.ts │ │ └── vite-env.d.ts │ ├── test-results/ │ │ └── .last-run.json │ ├── tests/ │ │ └── utils/ │ │ ├── calculateBoxBetween.ts │ │ ├── calculateHitArea.ts │ │ ├── debugging/ │ │ │ ├── logDebugState.ts │ │ │ └── logGroup.ts │ │ ├── expectLayout.ts │ │ ├── expectPanelSize.ts │ │ ├── getCenterCoordinates.ts │ │ ├── getSeparatorAriaAttributes.ts │ │ ├── goToUrl.ts │ │ ├── goToUrlWithIframe.ts │ │ ├── pointer-interactions/ │ │ │ └── resizeHelper.ts │ │ ├── serializer/ │ │ │ ├── decode.ts │ │ │ ├── encode.ts │ │ │ └── types.ts │ │ ├── types.ts │ │ └── updateUrl.ts │ ├── tsconfig.json │ └── vite.config.ts ├── lib/ │ ├── components/ │ │ ├── ErrorBoundary.test.tsx │ │ └── ErrorBoundary.tsx │ ├── context/ │ │ └── ErrorBoundaryContext.ts │ ├── hooks/ │ │ ├── useErrorBoundary.test.tsx │ │ └── useErrorBoundary.ts │ ├── index.ts │ ├── types.ts │ └── utils/ │ ├── assert.ts │ ├── assertErrorBoundaryContext.ts │ ├── getErrorMessage.ts │ ├── isErrorBoundaryContext.ts │ ├── withErrorBoundary.test.tsx │ └── withErrorBoundary.ts ├── package.json ├── pnpm-workspace.yaml ├── public/ │ ├── generated/ │ │ └── examples/ │ │ ├── AsyncUserCodeErrors.json │ │ ├── ErrorLogging.json │ │ ├── FallbackComponent.json │ │ ├── FallbackContent.json │ │ ├── GetErrorMessage.json │ │ ├── NpmResolution.json │ │ ├── RenderProp.json │ │ ├── ResetWithUseErrorBoundary.json │ │ ├── UseClient.json │ │ ├── UseErrorBoundary.json │ │ ├── WithErrorBoundaryA.json │ │ ├── WithErrorBoundaryB.json │ │ ├── WithErrorBoundaryC.json │ │ └── YarnResolution.json │ └── robots.txt ├── scripts/ │ ├── compile-docs.ts │ ├── compile-examples.ts │ └── compress-og-image ├── src/ │ ├── App.tsx │ ├── components/ │ │ ├── ContinueLink.tsx │ │ ├── Divider.tsx │ │ ├── Link.tsx │ │ └── NavLink.tsx │ ├── routes/ │ │ ├── AsyncUserCodeErrorsRoute.tsx │ │ ├── ErrorBoundaryPropsRoute.tsx │ │ ├── ErrorLoggingRoute.tsx │ │ ├── FallbackComponentRoute.tsx │ │ ├── FallbackContentRoute.tsx │ │ ├── GetErrorMessageRoute.tsx │ │ ├── RenderPropRoute.tsx │ │ ├── ResetNearestBoundaryRoute.tsx │ │ ├── UseErrorBoundaryRoute.tsx │ │ ├── WithErrorBoundaryRoute.tsx │ │ └── examples/ │ │ ├── AsyncUserCodeErrors.tsx │ │ ├── ErrorLogging.tsx │ │ ├── FallbackComponent.tsx │ │ ├── FallbackContent.tsx │ │ ├── GetErrorMessage.ts │ │ ├── NpmResolution.json │ │ ├── RenderProp.tsx │ │ ├── ResetWithUseErrorBoundary.tsx │ │ ├── UseClient.ts │ │ ├── UseErrorBoundary.tsx │ │ ├── WithErrorBoundaryA.tsx │ │ ├── WithErrorBoundaryB.tsx │ │ ├── WithErrorBoundaryC.tsx │ │ └── YarnResolution.json │ ├── routes.ts │ └── vite-env.d.ts ├── tsconfig.json ├── vercel.json ├── vite.config.ts ├── vitest.config.ts ├── vitest.d.ts └── vitest.setup.ts ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitattributes ================================================ * text=auto eol=lf ================================================ FILE: .github/workflows/eslint.yml ================================================ name: "ESLint" on: [pull_request] jobs: eslint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 - uses: pnpm/action-setup@v2 with: version: 10 - name: Install dependencies run: pnpm install --frozen-lockfile --recursive - name: Run ESLint run: pnpm lint ================================================ FILE: .github/workflows/pending-changes.yml ================================================ name: "Pending changes" on: [pull_request] jobs: pending-changes: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 - uses: pnpm/action-setup@v2 with: version: 10 - name: Install dependencies run: pnpm install --frozen-lockfile --recursive - uses: nickcharlton/diff-check@main with: command: pnpm run compile ================================================ FILE: .github/workflows/prettier.yml ================================================ name: "Prettier" on: [pull_request] jobs: prettier: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 - uses: pnpm/action-setup@v2 with: version: 10 - name: Install dependencies run: pnpm install --frozen-lockfile --recursive - name: Run Prettier run: pnpm run prettier:ci ================================================ FILE: .github/workflows/typescript.yml ================================================ name: "TypeScript" on: [pull_request] jobs: typescript: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 - uses: pnpm/action-setup@v2 with: version: 10 - name: Install dependencies run: pnpm install --frozen-lockfile --recursive - name: Build NPM package run: pnpm build - name: Run TypeScript run: pnpm tsc ================================================ FILE: .github/workflows/vitest.yml ================================================ name: "Vitest" on: [pull_request] jobs: unit-tests: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 - uses: pnpm/action-setup@v2 with: version: 10 - name: Install dependencies run: pnpm install --frozen-lockfile --recursive - name: Build NPM packages run: pnpm run build - name: Run tests run: pnpm run test:ci ================================================ FILE: .gitignore ================================================ dist docs node_modules .DS_Store .cache *.log .parcel-cache .pnp.* ================================================ FILE: .nvmrc ================================================ 18 ================================================ FILE: .prettierignore ================================================ /dist /docs /generated /public /src/routes/examples ================================================ FILE: CHANGELOG.md ================================================ # CHANGELOG See the [releases page](../../releases). ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards Examples of behavior that contributes to a positive environment for our community include: - Demonstrating empathy and kindness toward other people - Being respectful of differing opinions, viewpoints, and experiences - Giving and gracefully accepting constructive feedback - Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience - Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: - The use of sexualized language or imagery, and sexual attention or advances of any kind - Trolling, insulting or derogatory comments, and personal or political attacks - Public or private harassment - Publishing others' private information, such as a physical or email address, without their explicit permission - Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. ## Scope This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at me+coc@kentcdodds.com. All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. ## Enforcement Guidelines Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ### 2. Warning **Community Impact**: A violation through a single incident or series of actions. **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ### 3. Temporary Ban **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within the community. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing Thanks for your interest in contributing to this project! Here are a couple of guidelines to keep in mind before opening a Pull Request: - Please open a GitHub issue for discussion _before_ submitting any significant changes to this API (including new features or functionality). - Please don't submit code that has been written by code-generation tools such as Copilot or Claude. (There's nothing wrong with these tools, but I'd prefer them not be a part of this project.) ## Local development To get started: ```sh pnpm install ``` ### Running the documentation site locally The documentation site is a great place to test pending changes. It runs on localhost port 3000 and can be started by running: ```sh pnpm dev ``` ### Running tests locally To run unit tests locally: ```sh pnpm test ``` ### Updating assets Before submitting, also make sure to update generated docs/examples: ``` pnpm compile pnpm prettier pnpm lint ``` > [!NOTE] > If you forget this step, CI will remind you! ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2020 Brian Vaughn Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ react-error-boundary logo `react-error-boundary`: Reusable React [error boundary](https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary) component. Supports all React renderers (including React DOM and React Native). ### If you like this project, 🎉 [become a sponsor](https://github.com/sponsors/bvaughn/) or ☕ [buy me a coffee](http://givebrian.coffee/) ## Getting started ```sh # npm npm install react-error-boundary # pnpm pnpm add react-error-boundary # yarn yarn add react-error-boundary ``` ## FAQs Frequently asked questions can be found [here](https://react-error-boundary-lib.vercel.app/common-questions). ## API ### ErrorBoundary A reusable React [error boundary](https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary) component. Wrap this component around other React components to "catch" errors and render a fallback UI. This package is built on top of React [error boundaries](https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary), so it has all of the advantages and constraints of that API. This means that it can't catch errors during: - Server side rendering - Event handlers - Asynchronous code (including effects) ℹ️ The component provides several ways to render a fallback: `fallback`, `fallbackRender`, and `FallbackComponent`. Refer to the documentation to determine which is best for your application. ℹ️ This is a **client component**. You can only pass props to it that are serializeable or use it in files that have a `"use client";` directive. #### Required props None #### Optional props
Name Description
onError

Optional callback to enable e.g. logging error information to a server. @param error Value that was thrown; typically an instance of Error @param info React "component stack" identifying where the error was thrown

onReset

Optional callback to to be notified when an error boundary is "reset" so React can retry the failed render.

resetKeys

When changed, these keys will reset a triggered error boundary. This can be useful when an error condition may be tied to some specific state (that can be uniquely identified by key). See the the documentation for examples of how to use this prop.

fallback

Static content to render in place of an error if one is thrown.

<ErrorBoundary fallback={<div className="text-red">Something went wrong</div>} />
FallbackComponent

React component responsible for returning a fallback UI based on a thrown value.

<ErrorBoundary FallbackComponent={Fallback} />
fallbackRender

Render prop function responsible for returning a fallback UI based on a thrown value.

<ErrorBoundary fallbackRender={({ error, resetErrorBoundary }) => <div>...</div>} />
# FAQ ## `ErrorBoundary` cannot be used as a JSX component This error can be caused by a version mismatch between [react](https://npmjs.com/package/react) and [@types/react](https://npmjs.com/package/@types/react). To fix this, ensure that both match exactly, e.g.: If using NPM: ```json { ... "overrides": { "@types/react": "17.0.60" }, ... } ``` If using Yarn: ```json { ... "resolutions": { "@types/react": "17.0.60" }, ... } ``` --- [This blog post](https://kentcdodds.com/blog/use-react-error-boundary-to-handle-errors-in-react) shows more examples of how this package can be used, although it was written for the [version 3 API](https://github.com/bvaughn/react-error-boundary/releases/tag/v3.1.4). ================================================ FILE: eslint.config.js ================================================ import js from "@eslint/js"; import reactHooks from "eslint-plugin-react-hooks"; import reactRefresh from "eslint-plugin-react-refresh"; import { globalIgnores } from "eslint/config"; import globals from "globals"; import tseslint from "typescript-eslint"; export default tseslint.config([ globalIgnores(["dist", "docs", "public/generated"]), { files: ["**/*.{ts,tsx}"], ignores: ["**/examples/*.{ts,tsx}"], extends: [ js.configs.recommended, tseslint.configs.recommended, reactHooks.configs["recommended-latest"], reactRefresh.configs.vite, ], languageOptions: { ecmaVersion: 2020, globals: globals.browser, parserOptions: { tsconfigRootDir: import.meta.dirname, }, }, rules: { "no-restricted-imports": [ "error", { patterns: ["*/../lib/*", "node:test"], }, ], "no-restricted-properties": [ "error", { property: "clientHeight", message: "Using clientHeight is restricted; prefer offsetHeight or getBoundingClientRect()", }, { property: "clientWidth", message: "Using clientWidth is restricted; prefer offsetWidth or getBoundingClientRect()", }, ], "react-hooks/exhaustive-deps": [ "error", { additionalHooks: "useIsomorphicLayoutEffect", }, ], "@typescript-eslint/no-unused-vars": [ "error", { args: "all", argsIgnorePattern: "^_", caughtErrors: "all", caughtErrorsIgnorePattern: "^_", destructuredArrayIgnorePattern: "^_", varsIgnorePattern: "^_", ignoreRestSiblings: true, }, ], }, }, ]); ================================================ FILE: index.css ================================================ @source "node_modules/react-lib-tools"; @import "tailwindcss"; @import "react-lib-tools/styles.css"; @theme { --color-background-gradient-1: var(--color-fuchsia-400); --color-background-gradient-2: var(--color-purple-700); --color-background-gradient-3: var(--color-pink-500); --color-common-question-header: var(--color-fuchsia-200); --color-focus-1: var(--color-sky-300); --color-focus-2: var(--color-sky-400); --color-focus-3: var(--color-sky-600); } ================================================ FILE: index.html ================================================ react-error-boundary | runtime error handler
================================================ FILE: index.tsx ================================================ import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; import "./index.css"; import App from "./src/App.tsx"; createRoot(document.getElementById("root")!).render( , ); ================================================ FILE: integrations/vite/README.md ================================================ # React + TypeScript + Vite This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. Currently, two official plugins are available: - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh ## Expanding the ESLint configuration If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: ```js export default tseslint.config({ extends: [ // Remove ...tseslint.configs.recommended and replace with this ...tseslint.configs.recommendedTypeChecked, // Alternatively, use this for stricter rules ...tseslint.configs.strictTypeChecked, // Optionally, add this for stylistic rules ...tseslint.configs.stylisticTypeChecked, ], languageOptions: { // other options... parserOptions: { project: ['./tsconfig.node.json', './tsconfig.app.json'], tsconfigRootDir: import.meta.dirname, }, }, }) ``` You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: ```js // eslint.config.js import reactX from 'eslint-plugin-react-x' import reactDom from 'eslint-plugin-react-dom' export default tseslint.config({ plugins: { // Add the react-x and react-dom plugins 'react-x': reactX, 'react-dom': reactDom, }, rules: { // other rules... // Enable its recommended typescript rules ...reactX.configs['recommended-typescript'].rules, ...reactDom.configs.recommended.rules, }, }) ``` ================================================ FILE: integrations/vite/eslint.config.js ================================================ import js from "@eslint/js"; import globals from "globals"; import reactHooks from "eslint-plugin-react-hooks"; import reactRefresh from "eslint-plugin-react-refresh"; import tseslint from "typescript-eslint"; export default tseslint.config( { ignores: ["dist"] }, { extends: [js.configs.recommended, ...tseslint.configs.recommended], files: ["**/*.{ts,tsx}"], languageOptions: { ecmaVersion: 2020, globals: globals.browser, parserOptions: { tsconfigRootDir: import.meta.dirname, }, }, plugins: { "react-hooks": reactHooks, "react-refresh": reactRefresh, }, rules: { ...reactHooks.configs.recommended.rules, "@typescript-eslint/no-unused-vars": [ "error", { args: "all", argsIgnorePattern: "^_", caughtErrors: "all", caughtErrorsIgnorePattern: "^_", destructuredArrayIgnorePattern: "^_", varsIgnorePattern: "^_", ignoreRestSiblings: true, }, ], "react-refresh/only-export-components": [ "warn", { allowConstantExport: true }, ], }, }, ); ================================================ FILE: integrations/vite/index.html ================================================ [Vite] react-error-boundary integration
================================================ FILE: integrations/vite/package.json ================================================ { "name": "vite", "private": true, "version": "0.0.0", "type": "module", "scripts": { "dev": "vite --port 3012", "build": "tsc -b && vite build", "test": "npx playwright test", "preview": "vite preview" }, "dependencies": { "react": "^19.2.3", "react-dom": "^19.2.3", "react-error-boundary": "workspace:*", "react-router": "^7" }, "devDependencies": { "@eslint/js": "^9.25.0", "@playwright/test": "^1", "@tailwindcss/vite": "^4.1.17", "@types/react": "^19.1.2", "@types/react-dom": "^19.1.2", "@vitejs/plugin-react": "^4.4.1", "eslint": "^9.25.0", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.19", "globals": "^16.0.0", "tailwindcss": "^4.1.17", "typescript": "~5.8.3", "typescript-eslint": "^8.30.1", "vite": "^6.3.5" } } ================================================ FILE: integrations/vite/playwright.config.ts ================================================ import { defineConfig, devices } from "@playwright/test"; export default defineConfig({ projects: [ { name: "chromium", timeout: 5_000, use: { ...devices["Desktop Chrome"], viewport: { width: 1000, height: 600 }, // Uncomment to visually debug // headless: false, // launchOptions: { // slowMo: 500 // } }, }, ], }); ================================================ FILE: integrations/vite/src/components/Children.tsx ================================================ import { useLayoutEffect, useState } from "react"; export type SizeProps = { height: number | undefined; width: number | undefined; }; export const Children = function Children({ height, onCommitLogsChange, width, }: SizeProps & { onCommitLogsChange: (logs: SizeProps[]) => void; }) { const [commitLogs, setCommitLogs] = useState([]); useLayoutEffect(() => { setCommitLogs((prev) => [ ...prev, { height: height === undefined ? undefined : parseFloat(height.toFixed(1)), width: width === undefined ? undefined : parseFloat(width.toFixed(1)), } as SizeProps, ]); }, [height, width]); useLayoutEffect(() => onCommitLogsChange(commitLogs)); // Account for StrictMode double rendering on mount useLayoutEffect( () => () => { setCommitLogs([]); }, [], ); return (
{width} x {height} pixels
); }; ================================================ FILE: integrations/vite/src/components/Container.tsx ================================================ import { type PropsWithChildren } from "react"; export type ContainerProps = PropsWithChildren<{ className?: string | undefined; }>; export function Container({ children, className }: ContainerProps) { return
{children}
; } ================================================ FILE: integrations/vite/src/components/DebugData.tsx ================================================ import { cn } from "../utils/cn"; export function DebugData({ data }: { data: object }) { return (
      {JSON.stringify(data, replacer, 2)}
    
); } function replacer(_key: string, value: unknown) { if (typeof value === "number") { return Math.round(value); } return value; } ================================================ FILE: integrations/vite/src/components/Resizer.tsx ================================================ import { type PropsWithChildren } from "react"; export type ResizerProps = PropsWithChildren; export function Resizer({ children: childrenProp }: ResizerProps) { // TODO return childrenProp; } ================================================ FILE: integrations/vite/src/index.css ================================================ @import "tailwindcss"; @layer base { h1 { @apply mb-4 text-4xl font-bold tracking-tight text-gray-900; } ul { @apply list-disc pl-6; } ol { @apply list-decimal pl-6; } p { @apply mb-2 mt-2; } a { @apply text-blue-600 hover:text-pink-400 visited:text-blue-900; } } #root { height: 100vh; } ================================================ FILE: integrations/vite/src/main.tsx ================================================ import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; import { BrowserRouter, Route, Routes } from "react-router"; import "./index.css"; import { Home } from "./routes/Home"; createRoot(document.getElementById("root")!).render( } /> , ); ================================================ FILE: integrations/vite/src/routes/Home.tsx ================================================ export function Home() { return
Coming soon...
; } ================================================ FILE: integrations/vite/src/utils/assert.ts ================================================ export function assert( expectedCondition: unknown, message: string = "Assertion error", ): asserts expectedCondition { if (!expectedCondition) { console.error(message); throw Error(message); } } ================================================ FILE: integrations/vite/src/utils/cn.ts ================================================ import { type ClassValue, clsx } from "clsx"; import { twMerge } from "tailwind-merge"; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } ================================================ FILE: integrations/vite/src/vite-env.d.ts ================================================ /// ================================================ FILE: integrations/vite/test-results/.last-run.json ================================================ { "status": "passed", "failedTests": [] } ================================================ FILE: integrations/vite/tests/utils/calculateBoxBetween.ts ================================================ import type { Box } from "./types"; export function calculateBoxBetween(boxA: Box, boxB: Box): Box { if (boxA.y === boxB.y) { return { x: boxA.x + boxA.width, y: boxA.y, height: boxA.height, width: boxB.x - (boxA.x + boxA.width), }; } else { return { x: boxA.x, y: boxA.y + boxA.height, height: boxB.y - (boxA.y + boxA.height), width: boxA.width, }; } } ================================================ FILE: integrations/vite/tests/utils/calculateHitArea.ts ================================================ import type { Page } from "@playwright/test"; import { calculateBoxBetween } from "./calculateBoxBetween"; export async function calculateHitArea(page: Page, panelIds: [string, string]) { const panelA = page.getByText(`id: ${panelIds[0]}`); const panelB = page.getByText(`id: ${panelIds[1]}`); const panelBoxA = (await panelA.boundingBox())!; const panelBoxB = (await panelB.boundingBox())!; return calculateBoxBetween(panelBoxA, panelBoxB); } ================================================ FILE: integrations/vite/tests/utils/debugging/logDebugState.ts ================================================ import type { Page } from "@playwright/test"; export async function logDebugState(page: Page, prefix?: string) { const string = await page.evaluate(() => Array.from(document.querySelectorAll("code")) .map((element) => element.outerHTML) .join("\n\n"), ); console.log(prefix ? `${prefix}\n\n${string}` : string); } ================================================ FILE: integrations/vite/tests/utils/debugging/logGroup.ts ================================================ import type { Page } from "@playwright/test"; export async function logGroup(page: Page) { console.log( await page.evaluate( () => document.querySelector("[data-group]")?.outerHTML, ), ); } ================================================ FILE: integrations/vite/tests/utils/expectLayout.ts ================================================ import { expect, type Page } from "@playwright/test"; import type { Layout } from "react-resizable-panels"; export async function expectLayout({ layout, mainPage, onLayoutCount, }: { layout: Layout; mainPage: Page; onLayoutCount: number; }) { await expect(mainPage.getByText('"layout"')).toHaveText( JSON.stringify( { layout, onLayoutCount, }, null, 2, ), ); } ================================================ FILE: integrations/vite/tests/utils/expectPanelSize.ts ================================================ import { expect, type Page } from "@playwright/test"; import type { PanelSize } from "react-resizable-panels"; export async function expectPanelSize({ mainPage, onResizeCount, panelId, panelSize, prevPanelSize, }: { mainPage: Page; onResizeCount: number; panelId: string | number; panelSize: PanelSize; prevPanelSize?: PanelSize | undefined; }) { const locator = mainPage.getByText(`"panelId": "${panelId}"`); const text = JSON.stringify( { panelId, onResizeCount, panelSize, prevPanelSize, }, null, 2, ); await expect(locator).toHaveText(text); } ================================================ FILE: integrations/vite/tests/utils/getCenterCoordinates.ts ================================================ import type { Box, Coordinates } from "./types"; export function getCenterCoordinates(box: Box): Coordinates { return { x: box.x + box.width / 2, y: box.y + box.height / 2, }; } ================================================ FILE: integrations/vite/tests/utils/getSeparatorAriaAttributes.ts ================================================ import type { Page } from "@playwright/test"; export async function getSeparatorAriaAttributes(page: Page, id?: string) { return page.evaluate( ([id]) => { const element = document.querySelector( id ? `[data-testid="${id}"]` : '[role="separator"]', ); return { "aria-controls": element?.getAttribute("aria-controls"), "aria-valuemax": element?.getAttribute("aria-valuemax"), "aria-valuemin": element?.getAttribute("aria-valuemin"), "aria-valuenow": element?.getAttribute("aria-valuenow"), }; }, [id], ); } ================================================ FILE: integrations/vite/tests/utils/goToUrl.ts ================================================ import type { Page } from "@playwright/test"; import { createElement, type ReactElement } from "react"; import { PopupWindow } from "../../src/components/PopupWindow"; import { encode } from "./serializer/encode"; export async function goToUrl( page: Page, elementProp: ReactElement, config: { useGroupCallbackRef?: boolean | undefined; useGroupRef?: boolean | undefined; usePanelCallbackRef?: boolean | undefined; usePanelRef?: boolean | undefined; usePopUpWindow?: boolean | undefined; } = {}, ): Promise { const { useGroupCallbackRef = false, useGroupRef = false, usePanelCallbackRef = false, usePanelRef = false, usePopUpWindow = false, } = config; let element = elementProp; let encodedString = ""; if (element) { if (usePopUpWindow) { element = createElement(PopupWindow, { children: element, className: "dark", }); } encodedString = encode(element); } const queryParams = [ useGroupCallbackRef ? "useGroupCallbackRef" : undefined, useGroupRef ? "useGroupRef" : undefined, usePanelCallbackRef ? "usePanelCallbackRef" : undefined, usePanelRef ? "usePanelRef" : undefined, ] .filter(Boolean) .join("&"); const url = new URL( `http://localhost:3012/e2e/decoder/${encodedString}?${queryParams}`, ); // Uncomment when testing for easier repro console.log("\n\n" + url.toString()); await page.goto(url.toString()); if (usePopUpWindow) { const popupPromise = page.waitForEvent("popup"); await page.getByRole("button").click(); return await popupPromise; } return page; } ================================================ FILE: integrations/vite/tests/utils/goToUrlWithIframe.ts ================================================ import type { Page } from "@playwright/test"; import type { ReactElement } from "react"; import type { GroupProps } from "react-resizable-panels"; import { encode } from "./serializer/encode"; export async function goToUrlWithIframe( page: Page, element: ReactElement, sameOrigin: boolean, ) { const encodedString = encode(element); const url = new URL("http://localhost:3012/e2e/decoder/iframe"); url.searchParams.set("urlPanelGroup", encodedString); if (sameOrigin) { url.searchParams.set("sameOrigin", ""); } // Uncomment when testing for easier repros // console.log(url.toString()); await page.goto(url.toString()); } ================================================ FILE: integrations/vite/tests/utils/pointer-interactions/resizeHelper.ts ================================================ import type { Page } from "@playwright/test"; import { calculateHitArea } from "../calculateHitArea"; import { getCenterCoordinates } from "../getCenterCoordinates"; export async function resizeHelper( page: Page, panelIds: [string, string], deltaX: number = 0, deltaY: number = 0, ) { const hitAreaBox = await calculateHitArea(page, panelIds); const centerCoordinates = getCenterCoordinates(hitAreaBox); const destinationCoordinates = { x: centerCoordinates.x + deltaX, y: centerCoordinates.y + deltaY, }; await page.mouse.move(centerCoordinates.x, centerCoordinates.y); await page.mouse.down(); await page.mouse.move(destinationCoordinates.x, destinationCoordinates.y, { steps: 1, }); await page.mouse.up(); } ================================================ FILE: integrations/vite/tests/utils/serializer/decode.ts ================================================ import { createElement, type ReactElement } from "react"; import type { GroupProps, PanelProps, SeparatorProps, } from "react-resizable-panels"; import { Container } from "../../../src/components/Container"; import { DisplayModeToggle } from "../../../src/components/DisplayModeToggle"; import { Group } from "../../../src/components/Group"; import { Panel } from "../../../src/components/Panel"; import { PopupWindow } from "../../../src/components/PopupWindow"; import { Separator } from "../../../src/components/Separator"; import type { EncodedContainerElement, EncodedDisplayModeToggleElement, EncodedElement, EncodedGroupElement, EncodedPanelElement, EncodedPopupWindowElement, EncodedSeparatorElement, EncodedTextElement, TextProps, } from "./types"; type Config = { groupProps?: Partial; panelProps?: Partial; }; let key = 0; export function decode(stringified: string, config: Config = {}) { const json = JSON.parse(stringified) as EncodedElement[]; return decodeChildren(json, config); } function decodeChildren( children: EncodedElement[], config: Config, ): ReactElement[] { const elements: ReactElement[] = []; children.forEach((current) => { if (!current) { return; } switch (current.type) { case "Container": { elements.push(decodeContainer(current, config)); break; } case "DisplayModeToggle": { elements.push(decodeDisplayModeToggle(current, config)); break; } case "Group": { elements.push(decodeGroup(current, config)); break; } case "Panel": { elements.push(decodePanel(current, config)); break; } case "PopupWindow": { elements.push(decodePopupWindow(current, config)); break; } case "Separator": { elements.push(decodeSeparator(current)); break; } case "Text": { elements.push(decodeText(current)); break; } default: { console.warn("Could not decode type:", current); } } }); return elements; } function decodeContainer( json: EncodedContainerElement, config: Config, ): ReactElement { const { children, ...props } = json.props; return createElement(Container, { key: ++key, ...props, ...config.panelProps, children: children ? decodeChildren(children, config) : undefined, }); } function decodeDisplayModeToggle( json: EncodedDisplayModeToggleElement, config: Config, ): ReactElement { const { children, ...props } = json.props; return createElement(DisplayModeToggle, { key: ++key, ...props, ...config.panelProps, children: children ? decodeChildren(children, config) : undefined, }); } function decodeGroup( json: EncodedGroupElement, config: Config, ): ReactElement { const { children, ...props } = json.props; return createElement(Group, { key: ++key, ...props, ...config.groupProps, children: children ? decodeChildren(children, config) : undefined, }); } function decodePanel( json: EncodedPanelElement, config: Config, ): ReactElement { const { children, ...props } = json.props; return createElement(Panel, { key: ++key, ...props, ...config.panelProps, children: children ? decodeChildren(children, config) : undefined, }); } function decodePopupWindow( json: EncodedPopupWindowElement, config: Config, ): ReactElement { const { children, ...props } = json.props; return createElement(PopupWindow, { key: ++key, ...props, ...config.panelProps, children: children ? decodeChildren(children, config) : undefined, }); } function decodeSeparator( json: EncodedSeparatorElement, ): ReactElement { return createElement(Separator, { key: ++key, ...json.props, }); } function decodeText(json: EncodedTextElement): ReactElement { return createElement("div", { key: ++key, ...json.props, }); } ================================================ FILE: integrations/vite/tests/utils/serializer/encode.ts ================================================ import { type PropsWithChildren, type ReactElement } from "react"; import { Group, Panel, Separator, type GroupProps, type PanelProps, type SeparatorProps, } from "react-resizable-panels"; import { Container, type ContainerProps, } from "../../../src/components/Container"; import { DisplayModeToggle, type DisplayModeToggleProps, } from "../../../src/components/DisplayModeToggle"; import { PopupWindow, type PopupWindowProps, } from "../../../src/components/PopupWindow"; import type { EncodedContainerElement, EncodedDisplayModeToggleElement, EncodedElement, EncodedGroupElement, EncodedPanelElement, EncodedPopupWindowElement, EncodedSeparatorElement, EncodedTextElement, TextProps, } from "./types"; export function encode(element: ReactElement) { const json = encodeChildren([element]); const stringified = JSON.stringify(json); return encodeURIComponent(stringified); } function encodeChildren(children: ReactElement[]): EncodedElement[] { const elements: EncodedElement[] = []; children.forEach((current) => { if (!current) { return; } switch (current.type) { case Container: { elements.push(encodeContainer(current as ReactElement)); break; } case DisplayModeToggle: { elements.push( encodeDisplayModeToggle( current as ReactElement, ), ); break; } case Group: { elements.push(encodeGroup(current as ReactElement)); break; } case Panel: { elements.push(encodePanel(current as ReactElement)); break; } case PopupWindow: { elements.push( encodePopupWindow(current as ReactElement), ); break; } case Separator: { elements.push(encodeSeparator(current as ReactElement)); break; } default: { if (typeof current === "object") { const { children } = current.props as TextProps; if (typeof children === "string") { elements.push(encodeTextChild(current as ReactElement)); } else { console.warn("Could not encode type:", current); } } } } }); return elements; } function encodeContainer( element: ReactElement, ): EncodedContainerElement { const { children, ...props } = element.props; const encodedChildren = encodeChildren( Array.isArray(children) ? children : [children], ); return { props: { ...props, children: encodedChildren.length > 0 ? encodedChildren : undefined, }, type: "Container", }; } function encodeDisplayModeToggle( element: ReactElement, ): EncodedDisplayModeToggleElement { const { children, ...props } = element.props; const encodedChildren = encodeChildren( Array.isArray(children) ? children : [children], ); return { props: { ...props, children: encodedChildren.length > 0 ? encodedChildren : undefined, }, type: "DisplayModeToggle", }; } function encodeGroup(element: ReactElement): EncodedGroupElement { const { children, onLayoutChange: _, ...props } = element.props; const encodedChildren = encodeChildren( Array.isArray(children) ? children : [children], ); return { props: { ...props, children: encodedChildren.length > 0 ? encodedChildren : undefined, }, type: "Group", }; } function encodePanel(element: ReactElement): EncodedPanelElement { const { children, onResize: __, ...props } = element.props; const encodedChildren = encodeChildren( Array.isArray(children) ? children : [children], ); return { props: { ...props, children: encodedChildren.length > 0 ? encodedChildren : undefined, }, type: "Panel", }; } function encodePopupWindow( element: ReactElement, ): EncodedPopupWindowElement { const { children, ...props } = element.props; const encodedChildren = encodeChildren( Array.isArray(children) ? children : [children], ); return { props: { ...props, children: encodedChildren.length > 0 ? encodedChildren : undefined, }, type: "PopupWindow", }; } function encodeSeparator( element: ReactElement, ): EncodedSeparatorElement { const { children: _, ...props } = element.props; return { type: "Separator", props, }; } function encodeTextChild(element: ReactElement): EncodedTextElement { return { props: { children: element.props.children, className: element.props.className, }, type: "Text", }; } ================================================ FILE: integrations/vite/tests/utils/serializer/types.ts ================================================ import type { GroupProps, PanelProps, SeparatorProps, } from "react-resizable-panels"; import type { ContainerProps } from "../../../src/components/Container"; import type { DisplayModeToggleProps } from "../../../src/components/DisplayModeToggle"; import type { PopupWindowProps } from "../../../src/components/PopupWindow"; type EncodedElementWithChildren = Omit< Props, "children" > & { children?: EncodedElement[] | undefined }; export interface EncodedContainerElement { props: EncodedElementWithChildren; type: "Container"; } export interface EncodedDisplayModeToggleElement { props: EncodedElementWithChildren; type: "DisplayModeToggle"; } export interface EncodedGroupElement { props: EncodedElementWithChildren; type: "Group"; } export interface EncodedPanelElement { props: EncodedElementWithChildren; type: "Panel"; } export interface EncodedPopupWindowElement { props: EncodedElementWithChildren; type: "PopupWindow"; } export interface EncodedSeparatorElement { props: SeparatorProps; type: "Separator"; } export type TextProps = { children: string; className?: string | undefined; }; export interface EncodedTextElement { props: TextProps; type: "Text"; } export type EncodedElement = | EncodedContainerElement | EncodedDisplayModeToggleElement | EncodedGroupElement | EncodedPanelElement | EncodedPopupWindowElement | EncodedSeparatorElement | EncodedTextElement; ================================================ FILE: integrations/vite/tests/utils/types.ts ================================================ export type Box = { x: number; y: number; width: number; height: number; }; export type Coordinates = { x: number; y: number; }; ================================================ FILE: integrations/vite/tests/utils/updateUrl.ts ================================================ import type { Page } from "@playwright/test"; import type { ReactElement } from "react"; import type { GroupProps } from "react-resizable-panels"; import { encode } from "./serializer/encode"; export async function updateUrl( page: Page, element: ReactElement | null, ) { const encodedString = element ? encode(element) : ""; await page.evaluate( ([encodedString]) => { const url = new URL(window.location.href); url.searchParams.set("urlPanelGroup", encodedString ?? ""); window.history.pushState( { urlPanelGroup: encodedString }, "", url.toString(), ); window.dispatchEvent(new Event("popstate")); }, [encodedString], ); } ================================================ FILE: integrations/vite/tsconfig.json ================================================ { "compilerOptions": { "target": "ES2020", "useDefineForClassFields": true, "lib": ["ES2020", "DOM", "DOM.Iterable"], "module": "ESNext", "skipLibCheck": true, "moduleResolution": "bundler", "allowImportingTsExtensions": true, "verbatimModuleSyntax": true, "moduleDetection": "force", "noEmit": true, "jsx": "react-jsx", "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "erasableSyntaxOnly": true, "noFallthroughCasesInSwitch": true, "noUncheckedSideEffectImports": true }, "include": ["src"] } ================================================ FILE: integrations/vite/vite.config.ts ================================================ import tailwindcss from "@tailwindcss/vite"; import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; // https://vite.dev/config/ export default defineConfig({ plugins: [react(), tailwindcss()], server: { cors: true, }, }); ================================================ FILE: lib/components/ErrorBoundary.test.tsx ================================================ import { createRef, type PropsWithChildren, type ReactElement, type RefObject, } from "react"; import { createRoot } from "react-dom/client"; import { act } from "react-dom/test-utils"; import { beforeEach, describe, expect, it, vi, type Mock } from "vitest"; import { assert } from "../utils/assert"; import { ErrorBoundary } from "./ErrorBoundary"; import type { ErrorBoundaryPropsWithComponent, ErrorBoundaryPropsWithFallback, ErrorBoundaryPropsWithRender, FallbackProps, OnErrorCallback, } from "../types"; import { getErrorMessage } from "../utils/getErrorMessage"; describe("ErrorBoundary", () => { let container: HTMLDivElement; let root: ReturnType; let shouldThrow = true; let valueToThrow: unknown; beforeEach(() => { // @ts-expect-error This is a React internal global.IS_REACT_ACT_ENVIRONMENT = true; // Don't clutter the console with expected error text vi.spyOn(console, "error").mockImplementation(() => { // No-op }); container = document.createElement("div"); root = createRoot(container); shouldThrow = false; valueToThrow = new Error("💥💥💥"); }); function MaybeThrows({ children }: PropsWithChildren) { if (shouldThrow) { throw valueToThrow; } return children; } it("should render children", () => { const container = document.createElement("div"); const root = createRoot(container); act(() => { root.render( Error}> Content , ); }); expect(container.textContent).toBe("Content"); }); describe("fallback props", () => { let errorBoundaryRef: RefObject; beforeEach(() => { errorBoundaryRef = createRef(); }); function render(props: Omit) { act(() => { root.render( Content , ); }); } it('should call "onError" prop if one is provided', () => { shouldThrow = true; const onError: Mock = vi.fn(); render({ onError }); expect(onError).toHaveBeenCalledTimes(1); expect(getErrorMessage(onError.mock.calls[0][0])).toEqual("💥💥💥"); }); it('should call "onReset" when boundary reset via imperative API', () => { shouldThrow = true; const onReset: Mock<(...args: unknown[]) => unknown> = vi.fn(); render({ onReset }); expect(onReset).not.toHaveBeenCalled(); act(() => errorBoundaryRef.current?.resetErrorBoundary("abc", 123)); expect(onReset).toHaveBeenCalledTimes(1); }); it('should call "onReset" when boundary reset via "resetKeys"', () => { shouldThrow = false; const onReset: Mock<(...args: unknown[]) => unknown> = vi.fn(); render({ onReset, resetKeys: [1] }); expect(onReset).not.toHaveBeenCalled(); // It should not be called if the keys change without an error render({ onReset, resetKeys: [2] }); expect(onReset).not.toHaveBeenCalled(); shouldThrow = true; render({ onReset, resetKeys: [2] }); expect(onReset).not.toHaveBeenCalled(); shouldThrow = false; render({ onReset, resetKeys: [3] }); expect(onReset).toHaveBeenCalledTimes(1); }); }); describe('"fallback" element', () => { function render( props: Omit = {}, ) { act(() => { root.render( Error}> Content , ); }); } it("should render fallback in the event of an error", () => { shouldThrow = true; render(); expect(container.textContent).toBe("Error"); }); it("should re-render children if boundary is reset reset keys", () => { shouldThrow = true; render({ resetKeys: [1] }); shouldThrow = false; expect(container.textContent).toBe("Error"); render({ resetKeys: [2] }); expect(container.textContent).toBe("Content"); }); it("should render a null fallback if specified", () => { shouldThrow = true; act(() => { root.render( Content , ); }); expect(container.textContent).toBe(""); }); }); describe('"FallbackComponent"', () => { let fallbackComponent: Mock<(props: FallbackProps) => ReactElement>; let lastRenderedError: unknown | null = null; let lastRenderedResetErrorBoundary: | FallbackProps["resetErrorBoundary"] | null = null; function render( props: Omit = {}, ) { act(() => { root.render( Content , ); }); } beforeEach(() => { lastRenderedError = null; lastRenderedResetErrorBoundary = null; fallbackComponent = vi.fn(); fallbackComponent.mockImplementation( ({ error, resetErrorBoundary }: FallbackProps) => { lastRenderedError = error; lastRenderedResetErrorBoundary = resetErrorBoundary; return
FallbackComponent
; }, ); }); it("should render fallback in the event of an error", () => { shouldThrow = true; render(); expect(getErrorMessage(lastRenderedError)).toBe("💥💥💥"); expect(container.textContent).toBe("FallbackComponent"); }); it("should re-render children if boundary is reset via prop", () => { shouldThrow = true; render(); expect(container.textContent).toBe("FallbackComponent"); expect(lastRenderedResetErrorBoundary).not.toBeNull(); act(() => { shouldThrow = false; assert(lastRenderedResetErrorBoundary !== null); lastRenderedResetErrorBoundary(); }); expect(container.textContent).toBe("Content"); }); it("should re-render children if boundary is reset reset keys", () => { shouldThrow = true; render({ resetKeys: [1] }); expect(container.textContent).toBe("FallbackComponent"); shouldThrow = false; render({ resetKeys: [2] }); expect(container.textContent).toBe("Content"); }); }); describe('"fallbackRender" render prop', () => { let lastRenderedError: unknown | null = null; let lastRenderedResetErrorBoundary: | FallbackProps["resetErrorBoundary"] | null = null; let fallbackRender: Mock<(props: FallbackProps) => ReactElement>; function render( props: Omit = {}, ) { act(() => { root.render( Content , ); }); } beforeEach(() => { lastRenderedError = null; lastRenderedResetErrorBoundary = null; fallbackRender = vi.fn(); fallbackRender.mockImplementation( ({ error, resetErrorBoundary }: FallbackProps) => { lastRenderedError = error; lastRenderedResetErrorBoundary = resetErrorBoundary; return
fallbackRender
; }, ); }); it("should render fallback in the event of an error", () => { shouldThrow = true; render(); expect(getErrorMessage(lastRenderedError)).toBe("💥💥💥"); expect(fallbackRender).toHaveBeenCalled(); expect(container.textContent).toBe("fallbackRender"); }); it("should re-render children if boundary is reset via prop", () => { shouldThrow = true; render(); expect(getErrorMessage(lastRenderedError)).toBe("💥💥💥"); expect(fallbackRender).toHaveBeenCalled(); expect(container.textContent).toBe("fallbackRender"); act(() => { shouldThrow = false; assert(lastRenderedResetErrorBoundary !== null); lastRenderedResetErrorBoundary(); }); expect(container.textContent).toBe("Content"); }); it("should re-render children if boundary is reset reset keys", () => { shouldThrow = true; render({ resetKeys: [1] }); expect(getErrorMessage(lastRenderedError)).toBe("💥💥💥"); expect(fallbackRender).toHaveBeenCalled(); expect(container.textContent).toBe("fallbackRender"); shouldThrow = false; render({ resetKeys: [2] }); expect(container.textContent).toBe("Content"); }); }); describe("thrown values", () => { let lastRenderedError: unknown | null = null; let fallbackRender: (props: FallbackProps) => ReactElement; let onError: Mock<(...args: unknown[]) => unknown>; beforeEach(() => { lastRenderedError = null; onError = vi.fn(); fallbackRender = ({ error }: FallbackProps) => { lastRenderedError = error; return
Error
; }; }); function render() { act(() => { root.render( Content , ); }); } it("should support thrown strings", () => { shouldThrow = true; valueToThrow = "String error"; render(); expect(lastRenderedError).toBe("String error"); expect(onError).toHaveBeenCalledTimes(1); expect(onError.mock.calls[0][0]).toEqual("String error"); expect(container.textContent).toBe("Error"); }); it("should support thrown null or undefined values", () => { shouldThrow = true; valueToThrow = null; render(); expect(lastRenderedError).toBe(null); expect(onError).toHaveBeenCalledTimes(1); expect(onError.mock.calls[0][0]).toEqual(null); expect(container.textContent).toBe("Error"); }); }); // TODO Various cases with resetKeys changing (length, order, etc) // TODO Errors thrown again after reset are caught // TODO Nested error boundaries if a fallback throws }); ================================================ FILE: lib/components/ErrorBoundary.tsx ================================================ import { Component, createElement, type ErrorInfo } from "react"; import { ErrorBoundaryContext } from "../context/ErrorBoundaryContext"; import type { ErrorBoundaryProps, FallbackProps } from "../types"; const isDevelopment = import.meta.env.DEV; type ErrorBoundaryState = | { didCatch: true; error: unknown; } | { didCatch: false; error: null; }; const initialState: ErrorBoundaryState = { didCatch: false, error: null, }; /** * A reusable React [error boundary](https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary) component. * Wrap this component around other React components to "catch" errors and render a fallback UI. * * This package is built on top of React [error boundaries](https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary), * so it has all of the advantages and constraints of that API. * This means that it can't catch errors during: * - Server side rendering * - Event handlers * - Asynchronous code (including effects) * * ℹ️ The component provides several ways to render a fallback: `fallback`, `fallbackRender`, and `FallbackComponent`. * Refer to the documentation to determine which is best for your application. * * ℹ️ This is a **client component**. You can only pass props to it that are serializeable or use it in files that have a `"use client";` directive. */ export class ErrorBoundary extends Component< ErrorBoundaryProps, ErrorBoundaryState > { constructor(props: ErrorBoundaryProps) { super(props); this.resetErrorBoundary = this.resetErrorBoundary.bind(this); this.state = initialState; } static getDerivedStateFromError(error: Error) { return { didCatch: true, error }; } resetErrorBoundary(...args: unknown[]) { const { error } = this.state; if (error !== null) { this.props.onReset?.({ args, reason: "imperative-api", }); this.setState(initialState); } } componentDidCatch(error: unknown, info: ErrorInfo) { this.props.onError?.(error, info); } componentDidUpdate( prevProps: ErrorBoundaryProps, prevState: ErrorBoundaryState, ) { const { didCatch } = this.state; const { resetKeys } = this.props; // There's an edge case where if the thing that triggered the error happens to *also* be in the resetKeys array, // we'd end up resetting the error boundary immediately. // This would likely trigger a second error to be thrown. // So we make sure that we don't check the resetKeys on the first call of cDU after the error is set. if ( didCatch && prevState.error !== null && hasArrayChanged(prevProps.resetKeys, resetKeys) ) { this.props.onReset?.({ next: resetKeys, prev: prevProps.resetKeys, reason: "keys", }); this.setState(initialState); } } render() { const { children, fallbackRender, FallbackComponent, fallback } = this.props; const { didCatch, error } = this.state; let childToRender = children; if (didCatch) { const props: FallbackProps = { error, resetErrorBoundary: this.resetErrorBoundary, }; if (typeof fallbackRender === "function") { childToRender = fallbackRender(props); } else if (FallbackComponent) { childToRender = createElement(FallbackComponent, props); } else if (fallback !== undefined) { childToRender = fallback; } else { if (isDevelopment) { console.error( "react-error-boundary requires either a fallback, fallbackRender, or FallbackComponent prop", ); } throw error; } } return createElement( ErrorBoundaryContext.Provider, { value: { didCatch, error, resetErrorBoundary: this.resetErrorBoundary, }, }, childToRender, ); } } function hasArrayChanged(a: unknown[] = [], b: unknown[] = []) { return ( a.length !== b.length || a.some((item, index) => !Object.is(item, b[index])) ); } ================================================ FILE: lib/context/ErrorBoundaryContext.ts ================================================ import { createContext } from "react"; export type ErrorBoundaryContextType = { didCatch: boolean; error: unknown | null; resetErrorBoundary: (...args: unknown[]) => void; }; export const ErrorBoundaryContext = createContext(null); ================================================ FILE: lib/hooks/useErrorBoundary.test.tsx ================================================ import { act, useLayoutEffect, type ReactNode } from "react"; import { createRoot } from "react-dom/client"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { ErrorBoundary } from "../components/ErrorBoundary"; import { assert } from "../utils/assert"; import { getErrorMessage } from "../utils/getErrorMessage"; import { useErrorBoundary, type UseErrorBoundaryApi } from "./useErrorBoundary"; describe("useErrorBoundary", () => { let container: HTMLDivElement; beforeEach(() => { vi.spyOn(console, "error").mockImplementation(() => { // Don't clutter the console with expected error text }); container = document.createElement("div"); }); function render(content: ReactNode) { const root = createRoot(container); act(() => { root.render(content); }); return root; } it("should activate and deactivate the nearest error boundary", () => { let resetBoundaryFn: UseErrorBoundaryApi["resetBoundary"] | null = null; let showBoundaryFn: UseErrorBoundaryApi["showBoundary"] | null = null; function Child() { const { resetBoundary, showBoundary } = useErrorBoundary(); useLayoutEffect(() => { resetBoundaryFn = resetBoundary; showBoundaryFn = showBoundary; }, [resetBoundary, showBoundary]); return
Child
; } render( (
Fallback: {getErrorMessage(error)}
)} >
, ); expect(container.textContent).toBe("Child"); act(() => { assert(showBoundaryFn != null); showBoundaryFn(new Error("Example")); }); expect(container.textContent).toBe("Fallback: Example"); act(() => { assert(resetBoundaryFn != null); resetBoundaryFn(); }); expect(container.textContent).toBe("Child"); }); it("should expose the current error to a fallback component", () => { const errorToThrow = new Error("Thrown"); function Child() { const { error } = useErrorBoundary(); expect(error).toBe(null); throw errorToThrow; return null; } function Fallback() { const { error } = useErrorBoundary(); expect(error).toBe(errorToThrow); return "Fallback"; } render( , ); expect(container.textContent).toBe("Fallback"); }); }); ================================================ FILE: lib/hooks/useErrorBoundary.ts ================================================ import { useContext, useMemo, useState } from "react"; import { ErrorBoundaryContext } from "../context/ErrorBoundaryContext"; import { assertErrorBoundaryContext } from "../utils/assertErrorBoundaryContext"; type UseErrorBoundaryState = | { error: unknown; hasError: true } | { error: null; hasError: false }; export type UseErrorBoundaryApi = { error: unknown | null; resetBoundary: () => void; showBoundary: (error: unknown) => void; }; /** * Convenience hook for imperatively showing or dismissing error boundaries. * * ⚠️ This hook must only be used within an `ErrorBoundary` subtree. */ export function useErrorBoundary(): { /** * The currently visible `Error` (if one has been thrown). */ error: unknown | null; /** * Method to reset and retry the nearest active error boundary (if one is active). */ resetBoundary: () => void; /** * Trigger the nearest error boundary to display the error provided. * * ℹ️ React only handles errors thrown during render or during component lifecycle methods (e.g. effects and did-mount/did-update). * Errors thrown in event handlers, or after async code has run, will not be caught. * This method is a way to imperatively trigger an error boundary during these phases. */ showBoundary: (error: unknown) => void; } { const context = useContext(ErrorBoundaryContext); assertErrorBoundaryContext(context); const { error, resetErrorBoundary } = context; const [state, setState] = useState({ error: null, hasError: false, }); const memoized = useMemo( () => ({ error, resetBoundary: () => { resetErrorBoundary(); setState({ error: null, hasError: false }); }, showBoundary: (error: unknown) => setState({ error, hasError: true, }), }), [error, resetErrorBoundary], ); if (state.hasError) { throw state.error; } return memoized; } ================================================ FILE: lib/index.ts ================================================ "use client"; export { ErrorBoundary } from "./components/ErrorBoundary"; export { ErrorBoundaryContext } from "./context/ErrorBoundaryContext"; export { useErrorBoundary } from "./hooks/useErrorBoundary"; export { getErrorMessage } from "./utils/getErrorMessage"; export { withErrorBoundary } from "./utils/withErrorBoundary"; export type { ErrorBoundaryContextType } from "./context/ErrorBoundaryContext"; export type { UseErrorBoundaryApi } from "./hooks/useErrorBoundary"; export type { ErrorBoundaryProps, ErrorBoundaryPropsWithComponent, ErrorBoundaryPropsWithFallback, ErrorBoundaryPropsWithRender, FallbackProps, OnErrorCallback, } from "./types"; ================================================ FILE: lib/types.ts ================================================ import type { ComponentType, ErrorInfo, PropsWithChildren, ReactNode, } from "react"; export type FallbackProps = { error: unknown; resetErrorBoundary: (...args: unknown[]) => void; }; export type OnErrorCallback = (error: unknown, info: ErrorInfo) => void; type ErrorBoundarySharedProps = PropsWithChildren<{ /** * Optional callback to enable e.g. logging error information to a server. * * @param error Value that was thrown; typically an instance of `Error` * @param info React "component stack" identifying where the error was thrown */ onError?: (error: unknown, info: ErrorInfo) => void; /** * Optional callback to to be notified when an error boundary is "reset" so React can retry the failed render. */ onReset?: ( details: | { reason: "imperative-api"; args: unknown[] } | { reason: "keys"; prev: unknown[] | undefined; next: unknown[] | undefined; }, ) => void; /** * When changed, these keys will reset a triggered error boundary. * This can be useful when an error condition may be tied to some specific state (that can be uniquely identified by key). * See the the documentation for examples of how to use this prop. */ resetKeys?: unknown[]; }>; export type ErrorBoundaryPropsWithComponent = ErrorBoundarySharedProps & { fallback?: never; /** * React component responsible for returning a fallback UI based on a thrown value. * * ```tsx * * ``` */ FallbackComponent: ComponentType; fallbackRender?: never; }; export type ErrorBoundaryPropsWithRender = ErrorBoundarySharedProps & { fallback?: never; FallbackComponent?: never; /** * [Render prop](https://react.dev/reference/react/Children#calling-a-render-prop-to-customize-rendering) function responsible for returning a fallback UI based on a thrown value. * * ```tsx *
...
} /> * ``` */ fallbackRender: (props: FallbackProps) => ReactNode; }; export type ErrorBoundaryPropsWithFallback = ErrorBoundarySharedProps & { /** * Static content to render in place of an error if one is thrown. * * ```tsx * Something went wrong} /> * ``` */ fallback: ReactNode; FallbackComponent?: never; fallbackRender?: never; }; export type ErrorBoundaryProps = | ErrorBoundaryPropsWithFallback | ErrorBoundaryPropsWithComponent | ErrorBoundaryPropsWithRender; ================================================ FILE: lib/utils/assert.ts ================================================ export function assert( expectedCondition: unknown, message: string = "Assertion error", ): asserts expectedCondition { if (!expectedCondition) { throw Error(message); } } ================================================ FILE: lib/utils/assertErrorBoundaryContext.ts ================================================ import type { ErrorBoundaryContextType } from "../context/ErrorBoundaryContext"; import { isErrorBoundaryContext } from "./isErrorBoundaryContext"; export function assertErrorBoundaryContext( value: unknown, ): asserts value is ErrorBoundaryContextType { if (!isErrorBoundaryContext(value)) { throw new Error("ErrorBoundaryContext not found"); } } ================================================ FILE: lib/utils/getErrorMessage.ts ================================================ export function getErrorMessage(thrown: unknown): string | undefined { switch (typeof thrown) { case "object": { if ( thrown !== null && "message" in thrown && typeof thrown.message === "string" ) { return thrown.message; } break; } case "string": { return thrown; } } } ================================================ FILE: lib/utils/isErrorBoundaryContext.ts ================================================ import type { ErrorBoundaryContextType } from "../context/ErrorBoundaryContext"; export function isErrorBoundaryContext( value: unknown, ): value is ErrorBoundaryContextType { return ( value !== null && typeof value === "object" && "didCatch" in value && typeof value.didCatch === "boolean" && "error" in value && "resetErrorBoundary" in value && typeof value.resetErrorBoundary === "function" ); } ================================================ FILE: lib/utils/withErrorBoundary.test.tsx ================================================ import { Component, createRef, type PropsWithChildren } from "react"; import { createRoot } from "react-dom/client"; import { act } from "react-dom/test-utils"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { withErrorBoundary } from "./withErrorBoundary"; describe("withErrorBoundary", () => { let container: HTMLDivElement; let root: ReturnType; let shouldThrow = true; let valueToThrow: unknown; beforeEach(() => { // @ts-expect-error This is a React internal global.IS_REACT_ACT_ENVIRONMENT = true; // Don't clutter the console with expected error text vi.spyOn(console, "error").mockImplementation(() => { // No-op }); container = document.createElement("div"); root = createRoot(container); shouldThrow = false; valueToThrow = new Error("💥💥💥"); }); function MaybeThrows({ children = "Children" }: PropsWithChildren) { if (shouldThrow) { throw valueToThrow; } return children; } function render() { const ErrorBoundary = withErrorBoundary(MaybeThrows, { fallback:
Error
, }); act(() => { root.render(); }); } it("should render children within the created HOC", () => { render(); expect(container.textContent).toBe("Children"); }); it("should catch errors with the created HOC", () => { shouldThrow = true; render(); expect(container.textContent).toBe("Error"); }); it("should forward refs", () => { type Props = { foo: string }; class Inner extends Component { test() { // No-op } render() { return this.props.foo; } } const Wrapped = withErrorBoundary(Inner, { fallback:
Error
, }); const ref = createRef(); act(() => { root.render(); }); expect(ref.current).not.toBeNull(); expect(typeof ref.current?.test).toBe("function"); }); }); ================================================ FILE: lib/utils/withErrorBoundary.ts ================================================ import { createElement, forwardRef, type ComponentClass, type ComponentType, } from "react"; import { ErrorBoundary } from "../components/ErrorBoundary"; import type { ErrorBoundaryProps } from "../types"; export function withErrorBoundary< Type extends ComponentClass, Props extends object, >(Component: ComponentType, errorBoundaryProps: ErrorBoundaryProps) { const Wrapped = forwardRef, Props>((props, ref) => createElement( ErrorBoundary, errorBoundaryProps, createElement(Component, { ...props, ref } as Props), ), ); // Format for display in DevTools const name = Component.displayName || Component.name || "Unknown"; Wrapped.displayName = `withErrorBoundary(${name})`; return Wrapped; } ================================================ FILE: package.json ================================================ { "name": "react-error-boundary", "version": "6.1.1", "type": "module", "description": "Simple reusable React error boundary component", "author": "Brian Vaughn ", "license": "MIT", "repository": { "type": "git", "url": "https://github.com/bvaughn/react-error-boundary" }, "contributors": [ "Brian Vaughn (https://github.com/bvaughn/)" ], "homepage": "https://react-error-boundary-lib.vercel.app/", "keywords": [ "react", "reactjs", "virtual", "window", "windowed", "list", "scrolling", "infinite", "virtualized", "table", "grid", "spreadsheet" ], "main": "dist/react-error-boundary.cjs", "module": "dist/react-error-boundary.js", "types": "dist/react-error-boundary.d.ts", "files": [ "dist" ], "scripts": { "dev": "vite", "dev:integrations": "pnpm -C integrations/vite/ run dev", "build": "pnpm run build:lib && pnpm run build:docs", "build:docs": "TARGET=docs vite build", "build:lib": "TARGET=lib vite build", "compile": "pnpm run compile:docs && pnpm run compile:examples", "compile:docs": "tsx ./scripts/compile-docs", "compile:examples": "tsx ./scripts/compile-examples", "compress:og-image": "tsx ./scripts/compress-og-image", "lint": "eslint .", "prerelease": "rm -rf dist && pnpm run build:lib", "prettier": "prettier --write \"**/*.{css,html,js,json,jsx,ts,tsx}\"", "prettier:ci": "prettier --check \"**/*.{css,html,js,json,jsx,ts,tsx}\"", "preview": "vite preview", "test": "vitest", "test:ci": "vitest run", "test:debug": "vitest --inspect-brk=127.0.0.1:3000 --no-file-parallelism", "tsc": "tsc -b" }, "lint-staged": { "**/*": "prettier --write --ignore-unknown" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0" }, "devDependencies": { "@csstools/postcss-oklab-function": "^4.0.11", "@eslint/js": "^9.30.1", "@headlessui/react": "^2.2.4", "@headlessui/tailwindcss": "^0.2.2", "@heroicons/react": "^2.2.0", "@tailwindcss/vite": "^4.1.11", "@tailwindplus/elements": "^1.0.5", "@testing-library/jest-dom": "^6.6.4", "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^14.6.1", "@types/bytes": "^3.1.5", "@types/compression": "^1.8.1", "@types/markdown-it": "^14.1.2", "@types/node": "^24.2.0", "@types/react": "^19.1.8", "@types/react-dom": "^19.2.3", "@types/sharp": "^0.32.0", "@vitejs/plugin-react-swc": "^3.10.2", "bytes": "^3.1.2", "clsx": "^2.1.1", "compression": "^1.8.1", "csstype": "^3.1.3", "eslint": "^9.30.1", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.20", "globals": "^16.3.0", "husky": "^9.1.7", "jsdom": "^26.1.0", "lint-staged": "^16.1.4", "markdown-it": "^14.1.0", "marked": "^16.4.1", "postcss": "^8.5.6", "prettier": "3.6.2", "prettier-plugin-tailwindcss": "^0.7.1", "react": "^19.2.3", "react-docgen-typescript": "^2.4.0", "react-dom": "^19.2.3", "react-error-boundary": "^6.0.0", "react-lib-tools": "^0.0.34", "react-router-dom": "^7.6.3", "rollup-plugin-terser": "^7.0.2", "rollup-plugin-visualizer": "^6.0.3", "rollup-preserve-directives": "^1.1.3", "sharp": "^0.34.5", "sirv": "^3.0.2", "tailwind-merge": "^3.3.1", "tailwindcss": "^4.1.11", "terser": "^5.43.1", "ts-blank-space": "^0.6.2", "ts-node": "^10.9.2", "tsx": "^4.21.0", "typescript": "~5.8.3", "typescript-eslint": "^8.35.1", "typescript-json-schema": "^0.65.1", "vite": "^7.0.4", "vite-plugin-dts": "^4.5.4", "vite-plugin-svgr": "^4.3.0", "vitest": "^3.2.4", "vitest-fail-on-console": "^0.10.1", "zustand": "^5.0.7" } } ================================================ FILE: pnpm-workspace.yaml ================================================ packages: - integrations/* - lib/* - src/* ================================================ FILE: public/generated/examples/AsyncUserCodeErrors.json ================================================ { "html": "
import { useErrorBoundary } from \"react-error-boundary\";
\n
 
\n
function useUserProfileInfo({ username }: { username: string }) {
\n
const { showBoundary } = useErrorBoundary();
\n
 
\n
useEffect(() => {
\n
fetchGreeting(username).then(
\n
(response) => {
\n
// Set data in state and re-render ...
\n
},
\n
(error) => {
\n
// Show error boundary
\n
showBoundary(error);
\n
}
\n
);
\n
});
\n
}
" } ================================================ FILE: public/generated/examples/ErrorLogging.json ================================================ { "html": "
import type { ErrorInfo } from \"react\";
\n
import { ErrorBoundary } from \"react-error-boundary\";
\n
 
\n
function logError(error: unknown, info: ErrorInfo) {
\n
// Do something with the error, e.g. log to an external API
\n
}
\n
 
\n
<ErrorBoundary FallbackComponent={ErrorFallback} onError={logError}>
\n
<YourApplication />
\n
</ErrorBoundary>;
" } ================================================ FILE: public/generated/examples/FallbackComponent.json ================================================ { "html": "
import { ErrorBoundary, getErrorMessage, type FallbackProps } from \"react-error-boundary\";
\n
 
\n
function Fallback({ error, resetErrorBoundary }: FallbackProps) {
\n
return (
\n
<div role=\"alert\">
\n
<p>Something went wrong:</p>
\n
<pre style={{ color: \"red\" }}>{getErrorMessage(error)}</pre>
\n
<button onClick={resetErrorBoundary}>Retry</button>
\n
</div>
\n
);
\n
}
\n
 
\n
<ErrorBoundary
\n
FallbackComponent={Fallback}
\n
onReset={(details) => {
\n
// Reset the state of your app so the error doesn't happen again
\n
}}
\n
>
\n
<YourApplication />
\n
</ErrorBoundary>;
" } ================================================ FILE: public/generated/examples/FallbackContent.json ================================================ { "html": "
import { ErrorBoundary } from \"react-error-boundary\";
\n
 
\n
<ErrorBoundary fallback={<div>Something went wrong</div>}>
\n
<YourApplication />
\n
</ErrorBoundary>;
" } ================================================ FILE: public/generated/examples/GetErrorMessage.json ================================================ { "html": "
import { getErrorMessage, type FallbackProps } from \"react-error-boundary\";
\n
 
\n
function Fallback({ error }: FallbackProps) {
\n
// Because 'error' can be anything, it's safest not to assume it's an Error
\n
// Use the getErrorMessage helper method to extract the message instead.
\n
const message = getErrorMessage(error) ?? \"Unknown error\";
\n
 
\n
// Render fallback UI...
\n
}
" } ================================================ FILE: public/generated/examples/NpmResolution.json ================================================ { "html": "
{
\n
\"overrides\": {
\n
\"@types/react\": \"17.0.60\"
\n
}
\n
}
" } ================================================ FILE: public/generated/examples/RenderProp.json ================================================ { "html": "
import { ErrorBoundary, getErrorMessage } from \"react-error-boundary\";
\n
 
\n
<ErrorBoundary
\n
fallbackRender={({ error, resetErrorBoundary }) => (
\n
<div role=\"alert\">
\n
<p>Something went wrong:</p>
\n
<pre style={{ color: \"red\" }}>{getErrorMessage(error)}</pre>
\n
<button onClick={resetErrorBoundary}>Retry</button>
\n
</div>
\n
)}
\n
onReset={(details) => {
\n
// Reset the state of your app so the error doesn't happen again
\n
}}
\n
>
\n
<YourApplication />
\n
</ErrorBoundary>;
" } ================================================ FILE: public/generated/examples/ResetWithUseErrorBoundary.json ================================================ { "html": "
import { useErrorBoundary } from \"react-error-boundary\";
\n
 
\n
function Example() {
\n
const { resetBoundary } = useErrorBoundary();
\n
 
\n
// Call resetBoundary() to reset the nearest ErrorBoundary and retry a failed render
\n
}
" } ================================================ FILE: public/generated/examples/UseClient.json ================================================ { "html": "
\"use client\";
\n
 
\n
// Imports and components code go here...
" } ================================================ FILE: public/generated/examples/UseErrorBoundary.json ================================================ { "html": "
import { useErrorBoundary } from \"react-error-boundary\";
\n
 
\n
function Example() {
\n
const {
\n
// The currently visible Error (if one has been thrown).
\n
error,
\n
 
\n
// Method to reset and retry the nearest active error boundary (if one is active).
\n
resetBoundary,
\n
 
\n
// Trigger the nearest error boundary to display the error provided.
\n
showBoundary,
\n
} = useErrorBoundary();
\n
 
\n
// ...
\n
}
" } ================================================ FILE: public/generated/examples/WithErrorBoundaryA.json ================================================ { "html": "
function UserProfile({ username }: { username: string }) {
\n
// Render...
\n
}
" } ================================================ FILE: public/generated/examples/WithErrorBoundaryB.json ================================================ { "html": "
import { withErrorBoundary } from \"react-error-boundary\";
\n
 
\n
const UserProfileWithErrorBoundary = withErrorBoundary(UserProfile, {
\n
fallback: <div>Something went wrong</div>,
\n
onError(error, info) {
\n
// Do something with the error
\n
// E.g. log to an error logging client here
\n
},
\n
});
" } ================================================ FILE: public/generated/examples/WithErrorBoundaryC.json ================================================ { "html": "
<UserProfileWithErrorBoundary username=\"Brian\" />;
" } ================================================ FILE: public/generated/examples/YarnResolution.json ================================================ { "html": "
{
\n
\"resolutions\": {
\n
\"@types/react\": \"17.0.60\"
\n
}
\n
}
" } ================================================ FILE: public/robots.txt ================================================ User-agent: * Allow: / ================================================ FILE: scripts/compile-docs.ts ================================================ import { compileDocs } from "react-lib-tools/scripts/compile-docs.ts"; await compileDocs({ componentNames: ["ErrorBoundary"], imperativeHandleNames: [], }); ================================================ FILE: scripts/compile-examples.ts ================================================ import { compileExamples } from "react-lib-tools/scripts/compile-examples.ts"; await compileExamples(); ================================================ FILE: scripts/compress-og-image ================================================ import { compressOgImage } from "react-lib-tools/scripts/compress-og-image.ts"; await compressOgImage(); ================================================ FILE: src/App.tsx ================================================ import { AppRoot, Callout, Code, ExternalLink, NavSection, type CommonQuestion, } from "react-lib-tools"; import { repository } from "../package.json"; import { html as htmlNpmResolution } from "../public/generated/examples/NpmResolution.json"; import { html as htmlYarnResolution } from "../public/generated/examples/YarnResolution.json"; import { Link } from "./components/Link"; import { NavLink } from "./components/NavLink"; import { routes } from "./routes"; export default function App() { return ( Getting started Fallback content Render prop Fallback component Error logging Async user code errors Retry nearest boundary ErrorBoundary useErrorBoundary hook withErrorBoundary HOC getErrorMessage helper Common questions Support } overview={ <>
React components and utils for managing runtime errors. Supports all React renderers (including React DOM and React Native).
} packageDescription="runtime error handling" packageName="react-error-boundary" repositoryUrl={repository.url} routes={routes} /> ); } const clientSideWarning = (
This package is built on top of React{" "} error boundaries , so it has all of the advantages and constraints of that API.
This means that it can't catch errors during:
  • Server side rendering
  • Event handlers
  • Asynchronous code (including effects)
You can show an error boundary for asynchronous code, but you have to catch the error yourself.{" "} Learn more.
); const commonQuestions: CommonQuestion[] = [ { id: "uncaught-error", question: "Why didn't the boundary catch my error?", answer: clientSideWarning, }, { id: "react-types-mismatch", question: ( <> ErrorBoundary cannot be used as a JSX component ), answer: ( <>

This error can be caused by a version mismatch between{" "} react and @types/react. To fix this, ensure that both match exactly.

For NPM, you may need to use an{" "} override :

Yarn has a similar mechanism called a{" "} resolution :

), }, ]; ================================================ FILE: src/components/ContinueLink.tsx ================================================ import type { Path } from "../routes"; import { Link } from "./Link"; export function ContinueLink({ title, to }: { title: string; to: Path }) { return (
Continue to {title}…
); } ================================================ FILE: src/components/Divider.tsx ================================================ export function Divider() { return
; } ================================================ FILE: src/components/Link.tsx ================================================ import type { HTMLAttributes } from "react"; import { Link as ExternalLink } from "react-lib-tools"; import type { Path } from "../routes"; export function Link({ to, ...rest }: HTMLAttributes & { to: Path; }) { return ; } ================================================ FILE: src/components/NavLink.tsx ================================================ import { type PropsWithChildren } from "react"; import { NavLink as NavLinkExternal, type DefaultPath } from "react-lib-tools"; import { type Path } from "../routes"; export function NavLink({ children, className, path, }: PropsWithChildren<{ className?: string | undefined; path: Path | DefaultPath; }>) { return ( ); } ================================================ FILE: src/routes/AsyncUserCodeErrorsRoute.tsx ================================================ import { Box, Code, Header, Link } from "react-lib-tools"; import { html } from "../../public/generated/examples/AsyncUserCodeErrors.json"; export default function AsyncUserCodeErrorsRoute() { return (
React only handles errors thrown during render or during component lifecycle methods (e.g. effects and did-mount/did-update). Errors thrown in event handlers, or after async code has run, will not be caught.
The useErrorBoundary hook can be used to pass those errors to the nearest error boundary:
); } ================================================ FILE: src/routes/ErrorBoundaryPropsRoute.tsx ================================================ import { Box, ComponentProps, type ComponentMetadata } from "react-lib-tools"; import json from "../../public/generated/docs/ErrorBoundary.json"; export default function ErrorBoundaryPropsRoute() { return ( ); } ================================================ FILE: src/routes/ErrorLoggingRoute.tsx ================================================ import { Box, Code, Header } from "react-lib-tools"; import { html } from "../../public/generated/examples/ErrorLogging.json"; export default function ErrorLoggingRoute() { return (
Use the onError callback to log errors to a service like Sentry.
); } ================================================ FILE: src/routes/FallbackComponentRoute.tsx ================================================ import { Box, Code, Header } from "react-lib-tools"; import { html } from "../../public/generated/examples/FallbackComponent.json"; export default function FallbackComponentRoute() { return (
React component responsible for returning a fallback UI based on a thrown value.
); } ================================================ FILE: src/routes/FallbackContentRoute.tsx ================================================ import { Box, Code, Header } from "react-lib-tools"; import { html } from "../../public/generated/examples/FallbackContent.json"; export default function RenderPropRoute() { return (
The simplest way to render a default error message.
); } ================================================ FILE: src/routes/GetErrorMessageRoute.tsx ================================================ import { Box, Code, ExternalLink, Header } from "react-lib-tools"; import { html } from "../../public/generated/examples/GetErrorMessage.json"; export default function GetErrorMessageRoute() { return (
Typically thrown errors in JavaScript are instances of type{" "} Error, but{" "} this is not always the case . Any value can be thrown- strings, numbers, even null or{" "} undefined.
To simplify working with thrown values, this library exports a utility method called getErrorMessage.
); } ================================================ FILE: src/routes/RenderPropRoute.tsx ================================================ import { Box, Code, ExternalLink, Header } from "react-lib-tools"; import { html } from "../../public/generated/examples/RenderProp.json"; export default function RenderPropRoute() { return (
Render prop {" "} function responsible for returning a fallback UI based on a thrown value.
); } ================================================ FILE: src/routes/ResetNearestBoundaryRoute.tsx ================================================ import { Box, Code, Header } from "react-lib-tools"; import { html } from "../../public/generated/examples/ResetWithUseErrorBoundary.json"; import { Link } from "../components/Link"; export default function ResetNearestBoundaryRoute() { return (
The useErrorBoundary hook can be used to reset the nearest error boundary and retry a failed render attempt.
); } ================================================ FILE: src/routes/UseErrorBoundaryRoute.tsx ================================================ import { Box, Callout, Code, Header } from "react-lib-tools"; import { html } from "../../public/generated/examples/UseErrorBoundary.json"; export default function UseErrorBoundaryRoute() { return (
Convenience hook for imperatively showing or dismissing error boundaries.
This hook must only be used within an ErrorBoundary{" "} subtree. ); } ================================================ FILE: src/routes/WithErrorBoundaryRoute.tsx ================================================ import { Box, Code, ExternalLink, Header } from "react-lib-tools"; import { html as htmlA } from "../../public/generated/examples/WithErrorBoundaryA.json"; import { html as htmlB } from "../../public/generated/examples/WithErrorBoundaryB.json"; import { html as htmlC } from "../../public/generated/examples/WithErrorBoundaryC.json"; export default function WithErrorBoundaryRoute() { return (
This package can also be used as a{" "} higher-order component . For example, given a component like this:
You could use the withErrorBoundary HOC to create a wrapper component:
And then render it like this
This might be useful for certain types of reusable/library components.
); } ================================================ FILE: src/routes/examples/AsyncUserCodeErrors.tsx ================================================ import { useEffect } from "react"; // hidden import { useErrorBoundary } from "react-error-boundary"; function useUserProfileInfo({ username }: { username: string }) { const { showBoundary } = useErrorBoundary(); useEffect(() => { fetchGreeting(username).then( (response) => { // Set data in state and re-render ... response; // hidden }, (error) => { // Show error boundary showBoundary(error); } ); }); } // export { useUserProfileInfo }; async function fetchGreeting(_: string) {} ================================================ FILE: src/routes/examples/ErrorLogging.tsx ================================================ import type { ErrorInfo } from "react"; import { ErrorBoundary } from "react-error-boundary"; function logError(error: unknown, info: ErrorInfo) { // Do something with the error, e.g. log to an external API error; // hidden info; // hidden } ; // function ErrorFallback() { return null; } function YourApplication() { return null; } ================================================ FILE: src/routes/examples/FallbackComponent.tsx ================================================ import { ErrorBoundary, getErrorMessage, type FallbackProps } from "react-error-boundary"; function Fallback({ error, resetErrorBoundary }: FallbackProps) { return (

Something went wrong:

{getErrorMessage(error)}
); } { // Reset the state of your app so the error doesn't happen again details; // hidden }} > ; // function YourApplication() { return null; } ================================================ FILE: src/routes/examples/FallbackContent.tsx ================================================ import { ErrorBoundary } from "react-error-boundary"; Something went wrong}> ; // function YourApplication() { return null; } ================================================ FILE: src/routes/examples/GetErrorMessage.ts ================================================ import { getErrorMessage, type FallbackProps } from "react-error-boundary"; function Fallback({ error }: FallbackProps) { // Because 'error' can be anything, it's safest not to assume it's an Error // Use the getErrorMessage helper method to extract the message instead. const message = getErrorMessage(error) ?? "Unknown error"; // Render fallback UI... return message; // hidden } // export { Fallback }; ================================================ FILE: src/routes/examples/NpmResolution.json ================================================ { "overrides": { "@types/react": "17.0.60" } } ================================================ FILE: src/routes/examples/RenderProp.tsx ================================================ import { ErrorBoundary, getErrorMessage } from "react-error-boundary"; (

Something went wrong:

{getErrorMessage(error)}
)} onReset={(details) => { // Reset the state of your app so the error doesn't happen again details; // hidden }} >
; // function YourApplication() { return null; } ================================================ FILE: src/routes/examples/ResetWithUseErrorBoundary.tsx ================================================ import { useErrorBoundary } from "react-error-boundary"; function Example() { const { resetBoundary } = useErrorBoundary(); // Call resetBoundary() to reset the nearest ErrorBoundary and retry a failed render resetBoundary; // hidden } // export { Example }; ================================================ FILE: src/routes/examples/UseClient.ts ================================================ "use client"; // Imports and components code go here... ================================================ FILE: src/routes/examples/UseErrorBoundary.tsx ================================================ import { useErrorBoundary } from "react-error-boundary"; function Example() { const { // The currently visible Error (if one has been thrown). error, // Method to reset and retry the nearest active error boundary (if one is active). resetBoundary, // Trigger the nearest error boundary to display the error provided. showBoundary, } = useErrorBoundary(); // ... error; // hidden resetBoundary; // hidden showBoundary; // hidden } // export { Example }; ================================================ FILE: src/routes/examples/WithErrorBoundaryA.tsx ================================================ function UserProfile({ username }: { username: string }) { username; // hidden return null; // hidden // Render... } // export { UserProfile }; ================================================ FILE: src/routes/examples/WithErrorBoundaryB.tsx ================================================ import { UserProfile } from "./WithErrorBoundaryA"; // import { withErrorBoundary } from "react-error-boundary"; const UserProfileWithErrorBoundary = withErrorBoundary(UserProfile, { fallback:
Something went wrong
, onError(error, info) { // Do something with the error // E.g. log to an error logging client here error; // hidden info; // hidden }, }); // export { UserProfileWithErrorBoundary }; ================================================ FILE: src/routes/examples/WithErrorBoundaryC.tsx ================================================ import { UserProfileWithErrorBoundary } from "./WithErrorBoundaryB"; // ; ================================================ FILE: src/routes/examples/YarnResolution.json ================================================ { "resolutions": { "@types/react": "17.0.60" } } ================================================ FILE: src/routes.ts ================================================ import { lazy, type ComponentType, type LazyExoticComponent } from "react"; export type Route = LazyExoticComponent>; export const routes = { "/examples/fallback": lazy(() => import("./routes/FallbackContentRoute")), "/examples/render-prop": lazy(() => import("./routes/RenderPropRoute")), "/examples/fallback-component": lazy( () => import("./routes/FallbackComponentRoute"), ), "/examples/error-logging": lazy(() => import("./routes/ErrorLoggingRoute")), "/examples/async-user-code-errors": lazy( () => import("./routes/AsyncUserCodeErrorsRoute"), ), "/examples/retry-nearest-boundary": lazy( () => import("./routes/ResetNearestBoundaryRoute"), ), "/api/error-boundary-props": lazy( () => import("./routes/ErrorBoundaryPropsRoute"), ), "/api/use-error-boundary-hook": lazy( () => import("./routes/UseErrorBoundaryRoute"), ), "/api/with-error-boundary-hoc": lazy( () => import("./routes/WithErrorBoundaryRoute"), ), "/api/get-error-message": lazy(() => import("./routes/GetErrorMessageRoute")), } satisfies Record; export type Routes = Record; export type Path = keyof Routes; ================================================ FILE: src/vite-env.d.ts ================================================ /// /// ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", "target": "ES2022", "useDefineForClassFields": true, "lib": ["ES2022", "DOM", "DOM.Iterable"], "module": "ESNext", "skipLibCheck": true, "moduleResolution": "bundler", "allowImportingTsExtensions": true, "verbatimModuleSyntax": true, "moduleDetection": "force", "noEmit": true, "jsx": "react-jsx", "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "erasableSyntaxOnly": true, "noFallthroughCasesInSwitch": true, "noUncheckedSideEffectImports": true, "exactOptionalPropertyTypes": true, "paths": { "react-error-boundary": ["./lib"] }, "types": [ "csstype", "@testing-library/jest-dom", "@testing-library/jest-dom/vitest" ] }, "include": ["vitest.d.ts", "lib", "scripts", "src", "index.tsx"], "exclude": [] } ================================================ FILE: vercel.json ================================================ { "rewrites": [ { "source": "/(.*)", "destination": "/index.html" } ] } ================================================ FILE: vite.config.ts ================================================ import tailwindcss from "@tailwindcss/vite"; import react from "@vitejs/plugin-react-swc"; import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { visualizer } from "rollup-plugin-visualizer"; import preserveDirectives from "rollup-preserve-directives"; import { defineConfig, type UserConfig } from "vite"; import dts from "vite-plugin-dts"; import svgr from "vite-plugin-svgr"; const __dirname = dirname(fileURLToPath(import.meta.url)); const libraryConfig: UserConfig = { build: { lib: { entry: resolve(__dirname, "lib/index.ts"), name: "react-error-boundary", fileName: "react-error-boundary", formats: ["cjs", "es"], }, rollupOptions: { external: ["react", "react-dom", "react/jsx-runtime"], }, sourcemap: true, terserOptions: { compress: { directives: false, }, }, }, plugins: [react(), dts({ rollupTypes: true }), preserveDirectives()], publicDir: false, }; const websiteConfig: UserConfig = { base: "/", build: { minify: "terser", outDir: "docs", sourcemap: true, terserOptions: { format: { comments: false, }, }, }, plugins: [ react(), svgr(), tailwindcss(), visualizer({ emitFile: true, filename: "stats.html", }), ], resolve: { alias: { "react-error-boundary": resolve(__dirname, "lib"), }, }, }; // Allow iPhone to connect to the DEV site using a local IP if (process.env.NODE_ENV === "development") { websiteConfig.server = { host: true, port: 3000, }; } let config: UserConfig = {}; switch (process.env.TARGET) { case "lib": { config = libraryConfig; break; } default: { config = websiteConfig; break; } } // https://vite.dev/config/ export default defineConfig(config); ================================================ FILE: vitest.config.ts ================================================ import { defineConfig, mergeConfig } from "vitest/config"; import viteConfig from "./vite.config"; export default mergeConfig( viteConfig, defineConfig({ test: { // onConsoleLog(log, type) { // console.log("[config] onConsoleLog:", type, log); // switch (type) { // case "stderr": { // throw Error("Unexpected console error: " + log); // } // } // }, environment: "jsdom", setupFiles: "./vitest.setup.js", exclude: ["node_modules", "integrations"], }, }), ); ================================================ FILE: vitest.d.ts ================================================ import "vitest"; declare module "vitest" { interface Matchers { toLogError: (expectedError: string) => ReturnType; } } ================================================ FILE: vitest.setup.ts ================================================ import "@testing-library/jest-dom/vitest"; import { cleanup } from "@testing-library/react"; import { afterAll, afterEach, beforeAll, expect, vi } from "vitest"; import failOnConsole from "vitest-fail-on-console"; const PROTOTYPE_PROPS = [ "clientHeight", "clientWidth", "offsetHeight", "offsetWidth", ]; failOnConsole({ shouldFailOnError: true, }); expect.addSnapshotSerializer({ serialize(value) { const rect = value as DOMRect; return `${rect.x}, ${rect.y} (${rect.width} x ${rect.height})`; }, test(value) { return ( value !== null && typeof value === "object" && "x" in value && "y" in value && "width" in value && "height" in value ); }, }); expect.extend({ toLogError: (callback: () => unknown, expectedError: string) => { const spy = vi.spyOn(console, "error").mockImplementation(() => {}); callback(); expect(console.error).toHaveBeenCalledWith(expectedError); spy.mockReset(); return { pass: true, message: () => "", }; }, }); beforeAll(() => { PROTOTYPE_PROPS.forEach((propertyKey) => { Object.defineProperty(HTMLElement.prototype, propertyKey, { configurable: true, value: 0, }); }); vi.spyOn(console, "warn").mockImplementation(() => { throw Error("Unexpectec console warning"); }); }); afterAll(() => { PROTOTYPE_PROPS.forEach((propertyKey) => { delete HTMLElement.prototype[ propertyKey as keyof typeof HTMLElement.prototype ]; }); }); afterEach(() => { cleanup(); });