diff --git a/demo/DemoApp.tsx b/demo/DemoApp.tsx index 53842a5..0cad5db 100644 --- a/demo/DemoApp.tsx +++ b/demo/DemoApp.tsx @@ -36,6 +36,8 @@ import {useNotify} from "../src/Components/Notifications/Notifications"; import {Notification, NotificationType} from "../src/Components/Notifications/Notification"; import {Box} from "../src/Components/Box"; import {Await, useAsync} from "../src/Async"; +import {NotifyErrorsBoundary} from "../src/Components/Errors/NotifyErrorsBoundary"; +import {DemoFailingComponent, DemoResetComponent} from "./DemoFailingComponent"; export function DemoApp() { @@ -68,6 +70,8 @@ export function DemoApp() }, 2000); }), [asyncChange]); + const [failingComponentsCount, setFailingComponentsCount] = useState(0); + return ( @@ -584,8 +588,20 @@ export function DemoApp()

Data: {data}

)} - - + + + + +

Error boundaries

+ + + + }> + { + [...Array(failingComponentsCount)].map(() => ) + } +
diff --git a/demo/DemoFailingComponent.tsx b/demo/DemoFailingComponent.tsx new file mode 100644 index 0000000..e2f3231 --- /dev/null +++ b/demo/DemoFailingComponent.tsx @@ -0,0 +1,26 @@ +import React from "react"; +import {useErrorBoundary} from "react-error-boundary"; + +/** + * A simple demo failing component. + */ +export function DemoFailingComponent() +{ + throw new Error("Proudly thrown error."); + return (

I will never be shown...

); +} + +/** + * A simple demo reset component. + */ +export function DemoResetComponent() +{ + // Get error boundary. + const errorBoundary = useErrorBoundary(); + + return ( + + ); +} diff --git a/index.ts b/index.ts index 3ed05de..04e8e5e 100644 --- a/index.ts +++ b/index.ts @@ -12,6 +12,7 @@ export {useCurtain} from "./src/Components/Curtains/CurtainInstance"; export type {CurtainContextState} from "./src/Components/Curtains/CurtainInstance"; export * from "./src/Components/Dates/Calendar"; export * from "./src/Components/Dates/Datepicker"; +export * from "./src/Components/Errors/NotifyErrorsBoundary"; export * from "./src/Components/Floating/Float"; export * from "./src/Components/Floating/Tooltip"; export * from "./src/Components/Forms/Checkbox"; diff --git a/package.json b/package.json index d935e29..9683d08 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "version": "1.5.0", + "version": "1.6.0", "name": "@kernelui/core", "description": "Kernel UI Core.", "scripts": { @@ -21,6 +21,7 @@ "@fontsource-variable/jetbrains-mono": "^5.0.21", "@fontsource-variable/manrope": "^5.0.20", "@fontsource-variable/source-serif-4": "^5.0.19", + "react-error-boundary": "^4.0.13", "react-merge-refs": "^2.1.1", "uuid": "^10.0.0" }, diff --git a/src/Components/Errors/NotifyErrorsBoundary.tsx b/src/Components/Errors/NotifyErrorsBoundary.tsx new file mode 100644 index 0000000..2bd22e0 --- /dev/null +++ b/src/Components/Errors/NotifyErrorsBoundary.tsx @@ -0,0 +1,36 @@ +import React, {useCallback, useMemo} from "react"; +import {ErrorBoundary, ErrorBoundaryProps} from "react-error-boundary"; +import {useNotify} from "../Notifications/Notifications"; +import {Notification, NotificationType} from "../Notifications/Notification"; + +/** + * A React error boundary that show errors in notifications. + */ +export function NotifyErrorsBoundary({children, onError, fallback, fallbackRender, FallbackComponent, ...props}: React.PropsWithChildren>) +{ + // Get notification function. + const notify = useNotify(); + + /** + * Error handling function. + */ + const handleError = useCallback((error: Error, info: React.ErrorInfo) => { + // Show a notification about the error. + notify(Unexpected error: {error.message}); + // Then call defined onError, if there is one. + onError?.(error, info); + }, [onError]); + + // Define default fallback component. + const defaultFallback = useMemo(() => <>, []); + + if (!fallback && !fallbackRender && !FallbackComponent) + // Set default fallback if nothing is set. + fallback = defaultFallback; + + return ( + + {children} + + ); +} diff --git a/yarn.lock b/yarn.lock index ee5391d..baa1c45 100644 --- a/yarn.lock +++ b/yarn.lock @@ -229,6 +229,15 @@ __metadata: languageName: node linkType: hard +"@babel/runtime@npm:^7.12.5": + version: 7.25.6 + resolution: "@babel/runtime@npm:7.25.6" + dependencies: + regenerator-runtime: "npm:^0.14.0" + checksum: 10c0/d6143adf5aa1ce79ed374e33fdfd74fa975055a80bc6e479672ab1eadc4e4bfd7484444e17dd063a1d180e051f3ec62b357c7a2b817e7657687b47313158c3d2 + languageName: node + linkType: hard + "@babel/template@npm:^7.24.6": version: 7.24.6 resolution: "@babel/template@npm:7.24.6" @@ -576,6 +585,7 @@ __metadata: less: "npm:^4.2.0" react: "npm:^18.3.1" react-dom: "npm:^18.3.1" + react-error-boundary: "npm:^4.0.13" react-merge-refs: "npm:^2.1.1" react-router-dom: "npm:^6.24.1" typescript: "npm:^5.4.5" @@ -2331,6 +2341,17 @@ __metadata: languageName: node linkType: hard +"react-error-boundary@npm:^4.0.13": + version: 4.0.13 + resolution: "react-error-boundary@npm:4.0.13" + dependencies: + "@babel/runtime": "npm:^7.12.5" + peerDependencies: + react: ">=16.13.1" + checksum: 10c0/6f3e0e4d7669f680ccf49c08c9571519c6e31f04dcfc30a765a7136c7e6fbbbe93423dd5a9fce12107f8166e54133e9dd5c2079a00c7a38201ac811f7a28b8e7 + languageName: node + linkType: hard + "react-merge-refs@npm:^2.1.1": version: 2.1.1 resolution: "react-merge-refs@npm:2.1.1" @@ -2378,6 +2399,13 @@ __metadata: languageName: node linkType: hard +"regenerator-runtime@npm:^0.14.0": + version: 0.14.1 + resolution: "regenerator-runtime@npm:0.14.1" + checksum: 10c0/1b16eb2c4bceb1665c89de70dcb64126a22bc8eb958feef3cd68fe11ac6d2a4899b5cd1b80b0774c7c03591dc57d16631a7f69d2daa2ec98100e2f29f7ec4cc4 + languageName: node + linkType: hard + "resolve@npm:~1.19.0": version: 1.19.0 resolution: "resolve@npm:1.19.0"