@dhlab/error-boundary
Version:
A universal React error boundary library that works with any router
369 lines (353 loc) • 20.7 kB
JavaScript
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