UNPKG

@dhlab/error-boundary

Version:

A universal React error boundary library that works with any router

369 lines (353 loc) 20.7 kB
import { jsx, jsxs } from 'react/jsx-runtime'; import { QueryErrorResetBoundary } from '@tanstack/react-query'; import { createContext, Component, createElement, useContext, useState, useMemo, forwardRef } from 'react'; class HTTPError extends Error { response; request; options; constructor(response, request, options) { const code = (response.status || response.status === 0) ? response.status : ''; const title = response.statusText || ''; const status = `${code} ${title}`.trim(); const reason = status ? `status code ${status}` : 'an unknown error'; super(`Request failed with ${reason}: ${request.method} ${request.url}`); this.name = 'HTTPError'; this.response = response; this.request = request; this.options = options; } } const ErrorBoundaryContext = createContext(null); const initialState = { didCatch: false, error: null }; let ErrorBoundary$1 = class ErrorBoundary extends Component { constructor(props) { super(props); this.resetErrorBoundary = this.resetErrorBoundary.bind(this); this.state = initialState; } static getDerivedStateFromError(error) { return { didCatch: true, error }; } resetErrorBoundary() { const { error } = this.state; if (error !== null) { var _this$props$onReset, _this$props; for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } (_this$props$onReset = (_this$props = this.props).onReset) === null || _this$props$onReset === void 0 ? void 0 : _this$props$onReset.call(_this$props, { args, reason: "imperative-api" }); this.setState(initialState); } } componentDidCatch(error, info) { var _this$props$onError, _this$props2; (_this$props$onError = (_this$props2 = this.props).onError) === null || _this$props$onError === void 0 ? void 0 : _this$props$onError.call(_this$props2, error, info); } componentDidUpdate(prevProps, prevState) { 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)) { var _this$props$onReset2, _this$props3; (_this$props$onReset2 = (_this$props3 = this.props).onReset) === null || _this$props$onReset2 === void 0 ? void 0 : _this$props$onReset2.call(_this$props3, { 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 = { 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 { throw error; } } return createElement(ErrorBoundaryContext.Provider, { value: { didCatch, error, resetErrorBoundary: this.resetErrorBoundary } }, childToRender); } }; function hasArrayChanged() { let a = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; let b = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : []; return a.length !== b.length || a.some((item, index) => !Object.is(item, b[index])); } function assertErrorBoundaryContext(value) { if (value == null || typeof value.didCatch !== "boolean" || typeof value.resetErrorBoundary !== "function") { throw new Error("ErrorBoundaryContext not found"); } } function useErrorBoundary() { const context = useContext(ErrorBoundaryContext); assertErrorBoundaryContext(context); const [state, setState] = useState({ error: null, hasError: false }); const memoized = useMemo(() => ({ resetBoundary: () => { context.resetErrorBoundary(); setState({ error: null, hasError: false }); }, showBoundary: error => setState({ error, hasError: true }) }), [context.resetErrorBoundary]); if (state.hasError) { throw state.error; } return memoized; } function withErrorBoundary(component, errorBoundaryProps) { const Wrapped = forwardRef((props, ref) => createElement(ErrorBoundary$1, errorBoundaryProps, createElement(component, { ...props, ref }))); // Format for display in DevTools const name = component.displayName || component.name || "Unknown"; Wrapped.displayName = "withErrorBoundary(".concat(name, ")"); return Wrapped; } const t=Symbol.for("@ts-pattern/matcher"),e=Symbol.for("@ts-pattern/isVariadic"),n="@ts-pattern/anonymous-select-key",r=t=>Boolean(t&&"object"==typeof t),i=e=>e&&!!e[t],o=(n,s,c)=>{if(i(n)){const e=n[t](),{matched:r,selections:i}=e.match(s);return r&&i&&Object.keys(i).forEach(t=>c(t,i[t])),r}if(r(n)){if(!r(s))return false;if(Array.isArray(n)){if(!Array.isArray(s))return false;let t=[],r=[],a=[];for(const o of n.keys()){const s=n[o];i(s)&&s[e]?a.push(s):a.length?r.push(s):t.push(s);}if(a.length){if(a.length>1)throw new Error("Pattern error: Using `...P.array(...)` several times in a single pattern is not allowed.");if(s.length<t.length+r.length)return false;const e=s.slice(0,t.length),n=0===r.length?[]:s.slice(-r.length),i=s.slice(t.length,0===r.length?Infinity:-r.length);return t.every((t,n)=>o(t,e[n],c))&&r.every((t,e)=>o(t,n[e],c))&&(0===a.length||o(a[0],i,c))}return n.length===s.length&&n.every((t,e)=>o(t,s[e],c))}return Reflect.ownKeys(n).every(e=>{const r=n[e];return (e in s||i(a=r)&&"optional"===a[t]().matcherType)&&o(r,s[e],c);var a;})}return Object.is(s,n)},s=e=>{var n,o,a;return r(e)?i(e)?null!=(n=null==(o=(a=e[t]()).getSelectionKeys)?void 0:o.call(a))?n:[]:Array.isArray(e)?c(e,s):c(Object.values(e),s):[]},c=(t,e)=>t.reduce((t,n)=>t.concat(e(n)),[]);function a(...t){if(1===t.length){const[e]=t;return t=>o(e,t,()=>{})}if(2===t.length){const[e,n]=t;return o(e,n,()=>{})}throw new Error(`isMatching wasn't given the right number of arguments: expected 1 or 2, received ${t.length}.`)}function u(t){return Object.assign(t,{optional:()=>h(t),and:e=>m(t,e),or:e=>d(t,e),select:e=>void 0===e?y(t):y(e,t)})}function l(t){return Object.assign((t=>Object.assign(t,{[Symbol.iterator](){let n=0;const r=[{value:Object.assign(t,{[e]:true}),done:false},{done:true,value:void 0}];return {next:()=>{var t;return null!=(t=r[n++])?t:r.at(-1)}}}}))(t),{optional:()=>l(h(t)),select:e=>l(void 0===e?y(t):y(e,t))})}function h(e){return u({[t]:()=>({match:t=>{let n={};const r=(t,e)=>{n[t]=e;};return void 0===t?(s(e).forEach(t=>r(t,void 0)),{matched:true,selections:n}):{matched:o(e,t,r),selections:n}},getSelectionKeys:()=>s(e),matcherType:"optional"})})}const f=(t,e)=>{for(const n of t)if(!e(n))return false;return true},g=(t,e)=>{for(const[n,r]of t.entries())if(!e(r,n))return false;return true};function m(...e){return u({[t]:()=>({match:t=>{let n={};const r=(t,e)=>{n[t]=e;};return {matched:e.every(e=>o(e,t,r)),selections:n}},getSelectionKeys:()=>c(e,s),matcherType:"and"})})}function d(...e){return u({[t]:()=>({match:t=>{let n={};const r=(t,e)=>{n[t]=e;};return c(e,s).forEach(t=>r(t,void 0)),{matched:e.some(e=>o(e,t,r)),selections:n}},getSelectionKeys:()=>c(e,s),matcherType:"or"})})}function p(e){return {[t]:()=>({match:t=>({matched:Boolean(e(t))})})}}function y(...e){const r="string"==typeof e[0]?e[0]:void 0,i=2===e.length?e[1]:"string"==typeof e[0]?void 0:e[0];return u({[t]:()=>({match:t=>{let e={[null!=r?r:n]:t};return {matched:void 0===i||o(i,t,(t,n)=>{e[t]=n;}),selections:e}},getSelectionKeys:()=>[null!=r?r:n].concat(void 0===i?[]:s(i))})})}function v(t){return "number"==typeof t}function b(t){return "string"==typeof t}function w(t){return "bigint"==typeof t}const S=u(p(function(t){return true})),O=S,j=t=>Object.assign(u(t),{startsWith:e=>{return j(m(t,(n=e,p(t=>b(t)&&t.startsWith(n)))));var n;},endsWith:e=>{return j(m(t,(n=e,p(t=>b(t)&&t.endsWith(n)))));var n;},minLength:e=>j(m(t,(t=>p(e=>b(e)&&e.length>=t))(e))),length:e=>j(m(t,(t=>p(e=>b(e)&&e.length===t))(e))),maxLength:e=>j(m(t,(t=>p(e=>b(e)&&e.length<=t))(e))),includes:e=>{return j(m(t,(n=e,p(t=>b(t)&&t.includes(n)))));var n;},regex:e=>{return j(m(t,(n=e,p(t=>b(t)&&Boolean(t.match(n))))));var n;}}),K=j(p(b)),x=t=>Object.assign(u(t),{between:(e,n)=>x(m(t,((t,e)=>p(n=>v(n)&&t<=n&&e>=n))(e,n))),lt:e=>x(m(t,(t=>p(e=>v(e)&&e<t))(e))),gt:e=>x(m(t,(t=>p(e=>v(e)&&e>t))(e))),lte:e=>x(m(t,(t=>p(e=>v(e)&&e<=t))(e))),gte:e=>x(m(t,(t=>p(e=>v(e)&&e>=t))(e))),int:()=>x(m(t,p(t=>v(t)&&Number.isInteger(t)))),finite:()=>x(m(t,p(t=>v(t)&&Number.isFinite(t)))),positive:()=>x(m(t,p(t=>v(t)&&t>0))),negative:()=>x(m(t,p(t=>v(t)&&t<0)))}),E=x(p(v)),A=t=>Object.assign(u(t),{between:(e,n)=>A(m(t,((t,e)=>p(n=>w(n)&&t<=n&&e>=n))(e,n))),lt:e=>A(m(t,(t=>p(e=>w(e)&&e<t))(e))),gt:e=>A(m(t,(t=>p(e=>w(e)&&e>t))(e))),lte:e=>A(m(t,(t=>p(e=>w(e)&&e<=t))(e))),gte:e=>A(m(t,(t=>p(e=>w(e)&&e>=t))(e))),positive:()=>A(m(t,p(t=>w(t)&&t>0))),negative:()=>A(m(t,p(t=>w(t)&&t<0)))}),P=A(p(w)),T=u(p(function(t){return "boolean"==typeof t})),B=u(p(function(t){return "symbol"==typeof t})),_=u(p(function(t){return null==t})),k=u(p(function(t){return null!=t}));var N={__proto__:null,matcher:t,optional:h,array:function(...e){return l({[t]:()=>({match:t=>{if(!Array.isArray(t))return {matched:false};if(0===e.length)return {matched:true};const n=e[0];let r={};if(0===t.length)return s(n).forEach(t=>{r[t]=[];}),{matched:true,selections:r};const i=(t,e)=>{r[t]=(r[t]||[]).concat([e]);};return {matched:t.every(t=>o(n,t,i)),selections:r}},getSelectionKeys:()=>0===e.length?[]:s(e[0])})})},set:function(...e){return u({[t]:()=>({match:t=>{if(!(t instanceof Set))return {matched:false};let n={};if(0===t.size)return {matched:true,selections:n};if(0===e.length)return {matched:true};const r=(t,e)=>{n[t]=(n[t]||[]).concat([e]);},i=e[0];return {matched:f(t,t=>o(i,t,r)),selections:n}},getSelectionKeys:()=>0===e.length?[]:s(e[0])})})},map:function(...e){return u({[t]:()=>({match:t=>{if(!(t instanceof Map))return {matched:false};let n={};if(0===t.size)return {matched:true,selections:n};const r=(t,e)=>{n[t]=(n[t]||[]).concat([e]);};if(0===e.length)return {matched:true};var i;if(1===e.length)throw new Error(`\`P.map\` wasn't given enough arguments. Expected (key, value), received ${null==(i=e[0])?void 0:i.toString()}`);const[s,c]=e;return {matched:g(t,(t,e)=>{const n=o(s,e,r),i=o(c,t,r);return n&&i}),selections:n}},getSelectionKeys:()=>0===e.length?[]:[...s(e[0]),...s(e[1])]})})},intersection:m,union:d,not:function(e){return u({[t]:()=>({match:t=>({matched:!o(e,t,()=>{})}),getSelectionKeys:()=>[],matcherType:"not"})})},when:p,select:y,any:S,_:O,string:K,number:E,bigint:P,boolean:T,symbol:B,nullish:_,nonNullable:k,instanceOf:function(t){return u(p(function(t){return e=>e instanceof t}(t)))},shape:function(t){return u(p(a(t)))}};class W extends Error{constructor(t){let e;try{e=JSON.stringify(t);}catch(n){e=t;}super(`Pattern matching error: no pattern matches value ${e}`),this.input=void 0,this.input=t;}}const $={matched:false,value:void 0};function z(t){return new I(t,$)}class I{constructor(t,e){this.input=void 0,this.state=void 0,this.input=t,this.state=e;}with(...t){if(this.state.matched)return this;const e=t[t.length-1],r=[t[0]];let i;3===t.length&&"function"==typeof t[1]?i=t[1]:t.length>2&&r.push(...t.slice(1,t.length-1));let s=false,c={};const a=(t,e)=>{s=true,c[t]=e;},u=!r.some(t=>o(t,this.input,a))||i&&!Boolean(i(this.input))?$:{matched:true,value:e(s?n in c?c[n]:c:this.input,this.input)};return new I(this.input,u)}when(t,e){if(this.state.matched)return this;const n=Boolean(t(this.input));return new I(this.input,n?{matched:true,value:e(this.input,this.input)}:$)}otherwise(t){return this.state.matched?this.state.value:t(this.input)}exhaustive(t=L){return this.state.matched?this.state.value:t(this.input)}run(){return this.exhaustive()}returnType(){return this}}function L(t){throw new W(t)} const HTTP_ERROR_ACTION_CONFIG = { goBack: { type: "go-back", message: "이전 페이지로 돌아가기", }, goLogin: { type: "go-login", message: "로그인 페이지로 이동", }, retry: { type: "retry", message: "다시 시도하기", }, goRoot: { type: "go-root", message: "홈으로 이동", }, }; const HTTP_CLIENT_ERROR_CONFIG = { 400: { type: "default", name: "Bad Request", message: "잘못된 요청입니다.", action: HTTP_ERROR_ACTION_CONFIG.goBack, }, 401: { type: "default", name: "Unauthorized", message: "로그인이 필요합니다.", action: HTTP_ERROR_ACTION_CONFIG.goLogin, }, 403: { type: "default", name: "Forbidden", message: "권한이 없습니다.", action: HTTP_ERROR_ACTION_CONFIG.goBack, }, 404: { type: "default", name: "Not Found", message: "요청하신 리소스를 찾을 수 없습니다.", action: HTTP_ERROR_ACTION_CONFIG.goBack, }, 409: { type: "default", name: "Conflict", message: "이미 존재하는 리소스입니다.", action: HTTP_ERROR_ACTION_CONFIG.goRoot, }, }; const HTTP_SERVER_ERROR_CONFIG = { 500: { type: "default", name: "Internal Server Error", message: "잠시 후 다시 시도해주세요.", action: HTTP_ERROR_ACTION_CONFIG.retry, }, }; const HTTP_ERROR_CONFIG = { ...HTTP_CLIENT_ERROR_CONFIG, ...HTTP_SERVER_ERROR_CONFIG, }; const getErrorConfig = (error, overrideConfig) => { var _a, _b, _c, _d; const statusCode = Object.keys(HTTP_ERROR_CONFIG).includes((_c = (_b = (_a = error.response) === null || _a === void 0 ? void 0 : _a.status) === null || _b === void 0 ? void 0 : _b.toString()) !== null && _c !== void 0 ? _c : "") ? (_d = error.response) === null || _d === void 0 ? void 0 : _d.status : 500; const defaultConfig = HTTP_ERROR_CONFIG[statusCode]; const override = overrideConfig === null || overrideConfig === void 0 ? void 0 : overrideConfig[statusCode]; return override !== null && override !== void 0 ? override : defaultConfig; }; const isAxiosError = (err) => { return (typeof err === "object" && err !== null && "isAxiosError" in err && err.isAxiosError === true); }; const isKyError = (err) => { return (typeof err === "object" && err !== null && "name" in err && err.name === "HTTPError" && "response" in err && err.response instanceof Response); }; const isApiError = (err) => { return isAxiosError(err) || isKyError(err); }; const DefaultButton = (props) => (jsx("button", { type: "button", className: `px-4 py-2 bg-gray-200 hover:bg-gray-300 rounded border ${props.className}`, ...props, children: props.children })); function ApiErrorBoundary({ children, FallbackContainer = ({ children }) => (jsx("div", { className: "h-full", children: children })), Button = DefaultButton, overrideConfig, resetKeys, ignoreError = [], }) { const handleError = (error, info) => { var _a; if (!isApiError(error)) { throw error; } const shouldIgnore = ignoreError.some((ignore) => z(ignore) .with(N.string, (ignoreStatusText) => { var _a; const statusText = error instanceof HTTPError ? error.response.statusText : ((_a = error.response) === null || _a === void 0 ? void 0 : _a.statusText) || ""; return statusText.match(ignoreStatusText); }) .with(N.number, (ignoreStatus) => { var _a; const status = error instanceof HTTPError ? error.response.status : ((_a = error.response) === null || _a === void 0 ? void 0 : _a.status) || 0; return status === ignoreStatus; }) .with(N.instanceOf(Function), (ignoreErrorFunction) => ignoreErrorFunction(error)) .otherwise(() => false)); if (shouldIgnore) { throw error; } const targetErrorConfig = getErrorConfig(error, overrideConfig); if (targetErrorConfig && "onError" in targetErrorConfig && targetErrorConfig.onError) { const status = error instanceof HTTPError ? error.response.status : ((_a = error.response) === null || _a === void 0 ? void 0 : _a.status) || 500; targetErrorConfig.onError(error, info, status); } }; return (jsx(QueryErrorResetBoundary, { children: ({ reset }) => (jsx(ErrorBoundary$1, { fallbackRender: ({ error, resetErrorBoundary }) => isApiError(error) ? (jsx(FallbackContainer, { children: jsx(ApiErrorFallback, { error: error, resetErrorBoundary: resetErrorBoundary, overrideConfig: overrideConfig, Button: Button }) })) : null, onError: handleError, onReset: reset, resetKeys: [ typeof window !== "undefined" ? window.location.pathname : "", ...(resetKeys !== null && resetKeys !== void 0 ? resetKeys : []), ], children: children })) })); } function ApiErrorFallback({ error, resetErrorBoundary, overrideConfig, Button, }) { const targetErrorConfig = getErrorConfig(error, overrideConfig); const handleActionButtonClick = () => { if (!targetErrorConfig || !("action" in targetErrorConfig) || !targetErrorConfig.action) { return; } z(targetErrorConfig.action.type) .with("go-back", () => { if (typeof window !== "undefined") { window.history.back(); } }) .with("go-login", () => { if (typeof window !== "undefined") { window.history.replaceState(null, "", "/login"); } }) .with("retry", () => { resetErrorBoundary(); }) .with("go-root", () => { if (typeof window !== "undefined") { window.history.replaceState(null, "", "/"); } }); }; return z(targetErrorConfig) .with({ type: "default", action: { message: N._ }, message: N._ }, ({ action, message }) => { return (jsxs("div", { className: "flex flex-col items-center justify-center h-full p-4 gap-y-2", children: [jsx("p", { className: "text-sm text-muted-foreground", children: message }), jsx(Button, { onClick: handleActionButtonClick, children: action.message })] })); }) .with({ type: "custom" }, (config) => { const { fallback } = config; if (typeof fallback === "function") { return fallback(error, resetErrorBoundary); } return fallback; }) .otherwise(() => null); } function ErrorBoundary({ children, ignoreError = [], onError, ...rest }) { const handleError = (error, errorInfo) => { const shouldIgnore = ignoreError.some((ignore) => { if (typeof ignore === "string") { return error.message.includes(ignore); } if (typeof ignore === "number") { return error.name === ignore.toString(); } if (typeof ignore === "function") { return ignore(error); } return false; }); if (shouldIgnore) { throw error; } onError === null || onError === void 0 ? void 0 : onError(error, errorInfo); }; return (jsx(ErrorBoundary$1, { ...rest, onError: handleError, children: children })); } export { ApiErrorBoundary, ErrorBoundary$1 as BaseErrorBoundary, ErrorBoundary, ErrorBoundaryContext, HTTP_ERROR_ACTION_CONFIG, HTTP_ERROR_CONFIG, getErrorConfig, useErrorBoundary, withErrorBoundary }; //# sourceMappingURL=index.esm.js.map