@suspensive/react
Version:
Suspensive interfaces for react
1 lines • 14.1 kB
Source Map (JSON)
{"version":3,"file":"ErrorBoundary.mjs","names":[],"sources":["../src/ErrorBoundary.tsx"],"sourcesContent":["'use client'\nimport {\n Component,\n type ComponentProps,\n type ComponentType,\n type ErrorInfo,\n type ForwardRefExoticComponent,\n type ForwardedRef,\n type FunctionComponent,\n type PropsWithChildren,\n type ReactNode,\n createContext,\n forwardRef,\n useContext,\n useImperativeHandle,\n useMemo,\n useRef,\n useState,\n} from 'react'\nimport { ErrorBoundaryGroupContext } from './ErrorBoundaryGroup'\nimport {\n Message_useErrorBoundaryFallbackProps_this_hook_should_be_called_in_ErrorBoundary_props_fallback,\n Message_useErrorBoundary_this_hook_should_be_called_in_ErrorBoundary_props_children,\n SuspensiveError,\n} from './models/SuspensiveError'\nimport type { ConstructorType } from './utility-types/ConstructorType'\nimport type { PropsWithoutChildren } from './utility-types/PropsWithoutChildren'\nimport { hasResetKeysChanged } from './utils/hasResetKeysChanged'\n\ninterface ErrorBoundaryHandle {\n /**\n * when you want to reset caught error, you can use this reset\n */\n reset: () => void\n}\n\nexport interface ErrorBoundaryFallbackProps<TError extends Error = Error> extends ErrorBoundaryHandle {\n /**\n * when ErrorBoundary catch error, you can use this error\n */\n error: TError\n}\n\ntype ErrorTypeGuard<TError extends Error> = (error: Error) => error is TError\ntype ErrorValidator = (error: Error) => boolean\n\ntype ErrorMatcher = boolean | ConstructorType<Error> | ErrorTypeGuard<Error> | ErrorValidator\n\ntype InferErrorByErrorMatcher<TErrorMatcher extends ErrorMatcher> =\n TErrorMatcher extends ConstructorType<infer TErrorOfConstructorType extends Error>\n ? TErrorOfConstructorType\n : TErrorMatcher extends ErrorTypeGuard<infer TErrorOfTypeGuard extends Error>\n ? TErrorOfTypeGuard\n : Error\n\ntype ShouldCatch = ErrorMatcher | [ErrorMatcher, ...ErrorMatcher[]]\n/**\n * Main type inference from shouldCatch\n */\ntype InferError<TShouldCatch extends ShouldCatch> = TShouldCatch extends readonly ErrorMatcher[]\n ? InferErrorByErrorMatcher<TShouldCatch[number]> extends never\n ? Error\n : InferErrorByErrorMatcher<TShouldCatch[number]>\n : TShouldCatch extends ErrorMatcher\n ? InferErrorByErrorMatcher<TShouldCatch>\n : Error\n\nconst matchError = (errorMatcher: ErrorMatcher, error: Error): error is InferError<typeof errorMatcher> => {\n if (typeof errorMatcher === 'boolean') {\n return errorMatcher\n }\n if (typeof errorMatcher === 'function') {\n // 1. Native Error constructor: prototype chain is intact\n try {\n if (errorMatcher === Error || errorMatcher.prototype instanceof Error) {\n return error instanceof errorMatcher\n }\n } catch {\n // If accessing prototype throws, it's not a constructor\n }\n // 2. Transpiled Error constructor: prototype chain is broken but instanceof still works\n try {\n if (error instanceof errorMatcher) {\n return true\n }\n } catch {\n // instanceof can throw if prototype is not an object (e.g., arrow functions)\n }\n // 3. Validator / type-guard function\n try {\n return (errorMatcher as ErrorValidator | ErrorTypeGuard<InferError<typeof errorMatcher>>)(error)\n } catch {\n return false\n }\n }\n return false\n}\n\nconst shouldCatchError = <TShouldCatch extends ShouldCatch>(\n shouldCatch: TShouldCatch | true,\n error: Error\n): error is InferError<TShouldCatch> =>\n Array.isArray(shouldCatch)\n ? shouldCatch.some((errorMatcher) => matchError(errorMatcher, error))\n : matchError(shouldCatch, error)\n\nexport type ErrorBoundaryProps<TShouldCatch extends ShouldCatch = true> = PropsWithChildren<{\n /**\n * an array of elements for the ErrorBoundary to check each render. If any of those elements change between renders, then the ErrorBoundary will reset the state which will re-render the children\n */\n resetKeys?: unknown[]\n /**\n * when ErrorBoundary is reset by resetKeys or fallback's props.reset, onReset will be triggered\n */\n onReset?: () => void\n /**\n * when ErrorBoundary catch error, onError will be triggered\n */\n onError?: (error: InferError<TShouldCatch>, info: ErrorInfo) => void\n /**\n * when ErrorBoundary catch error, fallback will be render instead of children\n */\n fallback: ReactNode | FunctionComponent<ErrorBoundaryFallbackProps<InferError<TShouldCatch>>>\n /**\n * determines whether the ErrorBoundary should catch errors based on conditions\n * @default true\n */\n shouldCatch?: TShouldCatch\n}>\n\ntype ErrorBoundaryState =\n | {\n isError: true\n error: Error\n }\n | {\n isError: false\n error: null\n }\n\nconst initialErrorBoundaryState: ErrorBoundaryState = {\n isError: false,\n error: null,\n}\n\nclass BaseErrorBoundary<TShouldCatch extends ShouldCatch> extends Component<\n ErrorBoundaryProps<TShouldCatch>,\n ErrorBoundaryState\n> {\n static getDerivedStateFromError(error: Error): ErrorBoundaryState {\n return { isError: true, error }\n }\n\n state = initialErrorBoundaryState\n\n componentDidUpdate(prevProps: ErrorBoundaryProps<TShouldCatch>, prevState: ErrorBoundaryState) {\n const { isError } = this.state\n const { resetKeys } = this.props\n if (isError && prevState.isError && hasResetKeysChanged(prevProps.resetKeys, resetKeys)) {\n this.reset()\n }\n }\n\n componentDidCatch(error: InferError<TShouldCatch>, info: ErrorInfo) {\n this.props.onError?.(error, info)\n }\n\n reset = () => {\n this.props.onReset?.()\n this.setState(initialErrorBoundaryState)\n }\n\n render() {\n const { children, fallback, shouldCatch = true } = this.props\n const { isError, error } = this.state\n\n let childrenOrFallback = children\n\n if (isError) {\n if (error instanceof SuspensiveError) {\n throw error\n }\n if (error instanceof ErrorInFallback) {\n throw error.originalError\n }\n if (!shouldCatchError(shouldCatch, error)) {\n throw error\n }\n\n if (typeof fallback === 'undefined') {\n if (process.env.NODE_ENV === 'development') {\n console.error('ErrorBoundary of @suspensive/react requires a defined fallback')\n }\n throw error\n }\n\n const Fallback = fallback\n childrenOrFallback = (\n <FallbackBoundary>\n {typeof Fallback === 'function' ? <Fallback error={error} reset={this.reset} /> : Fallback}\n </FallbackBoundary>\n )\n }\n\n return (\n <ErrorBoundaryContext.Provider value={{ ...this.state, reset: this.reset }}>\n {childrenOrFallback}\n </ErrorBoundaryContext.Provider>\n )\n }\n}\n\nclass ErrorInFallback extends Error {\n originalError: Error\n constructor(originalError: Error) {\n super()\n this.originalError = originalError\n }\n}\n\nclass FallbackBoundary extends Component<{ children: ReactNode }> {\n componentDidCatch(originalError: Error) {\n throw originalError instanceof SuspensiveError ? originalError : new ErrorInFallback(originalError)\n }\n render() {\n return this.props.children\n }\n}\n\n/**\n * This component provides a simple and reusable wrapper that you can use to wrap around your components. Any rendering errors in your components hierarchy can then be gracefully handled.\n * @see {@link https://suspensive.org/docs/react/ErrorBoundary Suspensive Docs}\n */\nexport const ErrorBoundary = Object.assign(\n forwardRef(function ErrorBoundary<TShouldCatch extends ShouldCatch>(\n props: ErrorBoundaryProps<TShouldCatch>,\n ref: ForwardedRef<ErrorBoundaryHandle>\n ) {\n const { fallback, children, onError, onReset, resetKeys, shouldCatch } = props\n const group = useContext(ErrorBoundaryGroupContext) ?? { resetKey: 0 }\n const baseErrorBoundaryRef = useRef<BaseErrorBoundary<TShouldCatch>>(null)\n useImperativeHandle(ref, () => ({\n reset: () => baseErrorBoundaryRef.current?.reset(),\n }))\n\n return (\n <BaseErrorBoundary<TShouldCatch>\n shouldCatch={shouldCatch}\n fallback={fallback}\n onError={onError}\n onReset={onReset}\n resetKeys={[group.resetKey, ...(resetKeys || [])]}\n ref={baseErrorBoundaryRef}\n >\n {children}\n </BaseErrorBoundary>\n )\n }) as {\n <TShouldCatch extends ShouldCatch>(\n props: ErrorBoundaryProps<TShouldCatch> & React.RefAttributes<ErrorBoundaryHandle>\n ): ReturnType<ForwardRefExoticComponent<ErrorBoundaryProps<TShouldCatch>>>\n },\n {\n displayName: 'ErrorBoundary',\n with: <\n TProps extends ComponentProps<ComponentType> = Record<string, never>,\n TShouldCatch extends ShouldCatch = ShouldCatch,\n >(\n errorBoundaryProps: PropsWithoutChildren<ErrorBoundaryProps<TShouldCatch>>,\n Component: ComponentType<TProps>\n ) =>\n Object.assign(\n (props: TProps) => (\n <ErrorBoundary<TShouldCatch> {...errorBoundaryProps}>\n <Component {...props} />\n </ErrorBoundary>\n ),\n { displayName: `${ErrorBoundary.displayName}.with(${Component.displayName || Component.name || 'Component'})` }\n ),\n Consumer: ({ children }: { children: (errorBoundary: ReturnType<typeof useErrorBoundary>) => ReactNode }) => (\n <>{children(useErrorBoundary())}</>\n ),\n }\n)\n\nconst ErrorBoundaryContext = Object.assign(createContext<(ErrorBoundaryHandle & ErrorBoundaryState) | null>(null), {\n displayName: 'ErrorBoundaryContext',\n})\n\n/**\n * This hook provides a simple and reusable wrapper that you can use to wrap around your components. Any rendering errors in your components hierarchy can then be gracefully handled.\n * @see {@link https://suspensive.org/docs/react/ErrorBoundary#useerrorboundary Suspensive Docs}\n */\n// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters\nexport const useErrorBoundary = <TError extends Error = Error>() => {\n const [state, setState] = useState<ErrorBoundaryState>({\n isError: false,\n error: null,\n })\n if (state.isError) {\n throw state.error\n }\n\n const errorBoundary = useContext(ErrorBoundaryContext)\n SuspensiveError.assert(\n errorBoundary != null && !errorBoundary.isError,\n Message_useErrorBoundary_this_hook_should_be_called_in_ErrorBoundary_props_children\n )\n\n return useMemo(\n () => ({\n setError: (error: TError) => setState({ isError: true, error }),\n }),\n []\n )\n}\n\n/**\n * This hook allows you to access the reset method and error objects without prop drilling.\n * @see {@link https://suspensive.org/docs/react/ErrorBoundary#useerrorboundaryfallbackprops Suspensive Docs}\n */\nexport const useErrorBoundaryFallbackProps = <TError extends Error = Error>(): ErrorBoundaryFallbackProps<TError> => {\n const errorBoundary = useContext(ErrorBoundaryContext)\n SuspensiveError.assert(\n errorBoundary != null && errorBoundary.isError,\n Message_useErrorBoundaryFallbackProps_this_hook_should_be_called_in_ErrorBoundary_props_fallback\n )\n\n return useMemo(\n () => ({\n error: errorBoundary.error as TError,\n reset: errorBoundary.reset,\n }),\n [errorBoundary.error, errorBoundary.reset]\n )\n}\n"],"mappings":";;;;;;;;;;AAmEA,MAAM,cAAc,cAA4B,UAA2D;CACzG,IAAI,OAAO,iBAAiB,WAC1B,OAAO;CAET,IAAI,OAAO,iBAAiB,YAAY;EAEtC,IAAI;GACF,IAAI,iBAAiB,SAAS,aAAa,qBAAqB,OAC9D,OAAO,iBAAiB;EAE5B,kBAAQ,CAER;EAEA,IAAI;GACF,IAAI,iBAAiB,cACnB,OAAO;EAEX,mBAAQ,CAER;EAEA,IAAI;GACF,OAAQ,aAAkF,KAAK;EACjG,mBAAQ;GACN,OAAO;EACT;CACF;CACA,OAAO;AACT;AAEA,MAAM,oBACJ,aACA,UAEA,MAAM,QAAQ,WAAW,IACrB,YAAY,MAAM,iBAAiB,WAAW,cAAc,KAAK,CAAC,IAClE,WAAW,aAAa,KAAK;AAoCnC,MAAM,4BAAgD;CACpD,SAAS;CACT,OAAO;AACT;AAEA,IAAM,oBAAN,cAAkE,UAGhE;;;EAKA,aAAQ;EAcR,mBAAc;;GACZ,2CAAK,OAAM,2FAAU;GACrB,KAAK,SAAS,yBAAyB;EACzC;;CArBA,OAAO,yBAAyB,OAAkC;EAChE,OAAO;GAAE,SAAS;GAAM;EAAM;CAChC;CAIA,mBAAmB,WAA6C,WAA+B;EAC7F,MAAM,EAAE,YAAY,KAAK;EACzB,MAAM,EAAE,cAAc,KAAK;EAC3B,IAAI,WAAW,UAAU,WAAW,oBAAoB,UAAU,WAAW,SAAS,GACpF,KAAK,MAAM;CAEf;CAEA,kBAAkB,OAAiC,MAAiB;;EAClE,4CAAK,OAAM,8FAAU,OAAO,IAAI;CAClC;CAOA,SAAS;EACP,MAAM,EAAE,UAAU,UAAU,cAAc,SAAS,KAAK;EACxD,MAAM,EAAE,SAAS,UAAU,KAAK;EAEhC,IAAI,qBAAqB;EAEzB,IAAI,SAAS;GACX,IAAI,iBAAiB,iBACnB,MAAM;GAER,IAAI,iBAAiB,iBACnB,MAAM,MAAM;GAEd,IAAI,CAAC,iBAAiB,aAAa,KAAK,GACtC,MAAM;GAGR,IAAI,OAAO,aAAa,aAAa;IACnC,IAAI,QAAQ,IAAI,aAAa,eAC3B,QAAQ,MAAM,gEAAgE;IAEhF,MAAM;GACR;GAEA,MAAM,WAAW;GACjB,qBACE,oBAAC,kBAAD,YACG,OAAO,aAAa,aAAa,oBAAC,UAAD;IAAiB;IAAO,OAAO,KAAK;GAAQ,KAAI,SAClE;EAEtB;EAEA,OACE,oBAAC,qBAAqB,UAAtB;GAA+B,yCAAY,KAAK,cAAO,OAAO,KAAK,OAAM;aACtE;EAC4B;CAEnC;AACF;AAEA,IAAM,kBAAN,cAA8B,MAAM;CAElC,YAAY,eAAsB;EAChC,MAAM;EACN,KAAK,gBAAgB;CACvB;AACF;AAEA,IAAM,mBAAN,cAA+B,UAAmC;CAChE,kBAAkB,eAAsB;EACtC,MAAM,yBAAyB,kBAAkB,gBAAgB,IAAI,gBAAgB,aAAa;CACpG;CACA,SAAS;EACP,OAAO,KAAK,MAAM;CACpB;AACF;;;;;AAMA,MAAa,gBAAgB,OAAO,OAClC,WAAW,SAAS,cAClB,OACA,KACA;;CACA,MAAM,EAAE,UAAU,UAAU,SAAS,SAAS,WAAW,gBAAgB;CACzE,MAAM,uBAAQ,WAAW,yBAAyB,sDAAK,EAAE,UAAU,EAAE;CACrE,MAAM,uBAAuB,OAAwC,IAAI;CACzE,oBAAoB,YAAY,EAC9B,aAAa;;uDAAqB,uFAAS,MAAM;GACnD,EAAE;CAEF,OACE,oBAAC,mBAAD;EACe;EACH;EACD;EACA;EACT,WAAW,CAAC,MAAM,UAAU,GAAI,aAAa,CAAC,CAAE;EAChD,KAAK;EAEJ;CACgB;AAEvB,CAAC,GAKD;CACE,aAAa;CACb,OAIE,oBACA,cAEA,OAAO,QACJ,UACC,oBAAC,iDAAgC,qCAC/B,oBAAC,8BAAc,KAAQ,IACV,IAEjB,EAAE,aAAa,GAAG,cAAc,YAAY,QAAQ,UAAU,eAAe,UAAU,QAAQ,YAAY,GAAG,CAChH;CACF,WAAW,EAAE,eACX,0CAAG,SAAS,iBAAiB,CAAC,EAAI;AAEtC,CACF;AAEA,MAAM,uBAAuB,OAAO,OAAO,cAAiE,IAAI,GAAG,EACjH,aAAa,uBACf,CAAC;;;;;AAOD,MAAa,yBAAuD;CAClE,MAAM,CAAC,OAAO,YAAY,SAA6B;EACrD,SAAS;EACT,OAAO;CACT,CAAC;CACD,IAAI,MAAM,SACR,MAAM,MAAM;CAGd,MAAM,gBAAgB,WAAW,oBAAoB;CACrD,gBAAgB,OACd,iBAAiB,QAAQ,CAAC,cAAc,SACxC,mFACF;CAEA,OAAO,eACE,EACL,WAAW,UAAkB,SAAS;EAAE,SAAS;EAAM;CAAM,CAAC,EAChE,IACA,CAAC,CACH;AACF;;;;;AAMA,MAAa,sCAAwG;CACnH,MAAM,gBAAgB,WAAW,oBAAoB;CACrD,gBAAgB,OACd,iBAAiB,QAAQ,cAAc,SACvC,gGACF;CAEA,OAAO,eACE;EACL,OAAO,cAAc;EACrB,OAAO,cAAc;CACvB,IACA,CAAC,cAAc,OAAO,cAAc,KAAK,CAC3C;AACF"}