From f41d3f22c7cf745fda48fe1be542174609a977dc Mon Sep 17 00:00:00 2001 From: Matiss Janis Aboltins <matiss@mja.lv> Date: Thu, 25 Apr 2024 19:31:04 +0100 Subject: [PATCH] :sparkles: improving error handling for lazy-loading (#2657) --- .../desktop-client/src/components/App.tsx | 2 +- .../src/components/FatalError.tsx | 52 ++++++++++++++----- .../src/components/common/Modal.tsx | 18 ++++--- .../components/gocardless/GoCardlessLink.tsx | 2 +- .../src/components/util/LoadComponent.tsx | 22 ++++++-- packages/loot-core/src/shared/errors.ts | 10 ++++ upcoming-release-notes/2657.md | 6 +++ 7 files changed, 85 insertions(+), 27 deletions(-) create mode 100644 upcoming-release-notes/2657.md diff --git a/packages/desktop-client/src/components/App.tsx b/packages/desktop-client/src/components/App.tsx index 85073350c..692f1986c 100644 --- a/packages/desktop-client/src/components/App.tsx +++ b/packages/desktop-client/src/components/App.tsx @@ -115,7 +115,7 @@ function ErrorFallback({ error }: FallbackProps) { return ( <> <AppBackground /> - <FatalError error={error} buttonText="Restart app" /> + <FatalError error={error} /> </> ); } diff --git a/packages/desktop-client/src/components/FatalError.tsx b/packages/desktop-client/src/components/FatalError.tsx index e0122a79e..58e2ec114 100644 --- a/packages/desktop-client/src/components/FatalError.tsx +++ b/packages/desktop-client/src/components/FatalError.tsx @@ -1,5 +1,6 @@ -// @ts-strict-ignore -import React, { useState } from 'react'; +import React, { useState, type ReactNode } from 'react'; + +import { LazyLoadFailedError } from 'loot-core/src/shared/errors'; import { Block } from './common/Block'; import { Button } from './common/Button'; @@ -19,16 +20,14 @@ type AppError = Error & { }; type FatalErrorProps = { - buttonText: string; error: Error | AppError; }; -type RenderSimpleProps = { - error: Error | AppError; -}; +type RenderSimpleProps = FatalErrorProps; function RenderSimple({ error }: RenderSimpleProps) { - let msg; + let msg: ReactNode; + if ('IDBFailure' in error && error.IDBFailure) { // IndexedDB wasn't able to open the database msg = ( @@ -93,6 +92,25 @@ function RenderSimple({ error }: RenderSimpleProps) { ); } +function RenderLazyLoadError() { + return ( + <Stack + style={{ + paddingBottom: 15, + lineHeight: '1.5em', + fontSize: 15, + }} + > + <Text> + There was a problem loading one of the chunks of the application. Please + reload the page and try again. If the issue persists - there might be an + issue with either your internet connection and/or the server where the + app is hosted. + </Text> + </Stack> + ); +} + function RenderUIError() { return ( <> @@ -151,26 +169,32 @@ function SharedArrayBufferOverride() { ); } -export function FatalError({ buttonText, error }: FatalErrorProps) { +export function FatalError({ error }: FatalErrorProps) { const [showError, setShowError] = useState(false); const showSimpleRender = 'type' in error && error.type === 'app-init-failure'; + const isLazyLoadError = error instanceof LazyLoadFailedError; return ( - <Modal isCurrent={true} CloseButton={undefined} title="Fatal Error"> + <Modal isCurrent title={isLazyLoadError ? 'Loading Error' : 'Fatal Error'}> <View style={{ maxWidth: 500, }} > - {showSimpleRender ? <RenderSimple error={error} /> : <RenderUIError />} + {isLazyLoadError ? ( + <RenderLazyLoadError /> + ) : showSimpleRender ? ( + <RenderSimple error={error} /> + ) : ( + <RenderUIError /> + )} + <Paragraph> - <Button onClick={() => window.Actual?.relaunch()}> - {buttonText} - </Button> + <Button onClick={() => window.Actual?.relaunch()}>Restart app</Button> </Paragraph> <Paragraph isLast={true} style={{ fontSize: 11 }}> - <Link variant="text" onClick={() => setShowError(true)}> + <Link variant="text" onClick={() => setShowError(state => !state)}> Show Error </Link> {showError && ( diff --git a/packages/desktop-client/src/components/common/Modal.tsx b/packages/desktop-client/src/components/common/Modal.tsx index 3fc6574b4..ddd498717 100644 --- a/packages/desktop-client/src/components/common/Modal.tsx +++ b/packages/desktop-client/src/components/common/Modal.tsx @@ -192,14 +192,16 @@ export const Modal = ({ </View> )} - <View - style={{ - position: 'absolute', - right: 0, - }} - > - <CloseButtonComponent onClick={onClose} /> - </View> + {onClose && ( + <View + style={{ + position: 'absolute', + right: 0, + }} + > + <CloseButtonComponent onClick={onClose} /> + </View> + )} </View> )} <View style={{ paddingTop: 0, flex: 1 }}> diff --git a/packages/desktop-client/src/components/gocardless/GoCardlessLink.tsx b/packages/desktop-client/src/components/gocardless/GoCardlessLink.tsx index 297763f0d..33d2fa81c 100644 --- a/packages/desktop-client/src/components/gocardless/GoCardlessLink.tsx +++ b/packages/desktop-client/src/components/gocardless/GoCardlessLink.tsx @@ -8,7 +8,7 @@ export function GoCardlessLink() { window.close(); return ( - <Modal isCurrent={true} CloseButton={undefined} title="Account sync"> + <Modal isCurrent title="Account sync"> {() => ( <View style={{ maxWidth: 500 }}> <Paragraph>Please wait...</Paragraph> diff --git a/packages/desktop-client/src/components/util/LoadComponent.tsx b/packages/desktop-client/src/components/util/LoadComponent.tsx index 9d4275ade..3a180373c 100644 --- a/packages/desktop-client/src/components/util/LoadComponent.tsx +++ b/packages/desktop-client/src/components/util/LoadComponent.tsx @@ -2,6 +2,8 @@ import { type ComponentType, useEffect, useState } from 'react'; import promiseRetry from 'promise-retry'; +import { LazyLoadFailedError } from 'loot-core/src/shared/errors'; + import { AnimatedLoading } from '../../icons/AnimatedLoading'; import { theme, styles } from '../../style'; import { Block } from '../common/Block'; @@ -19,22 +21,36 @@ export function LoadComponent<K extends string>(props: LoadComponentProps<K>) { return <LoadComponentInner key={props.name} {...props} />; } +// Cache of the various modules so we would not need to +// load the same thing multiple times. +const localModuleCache = new Map(); + function LoadComponentInner<K extends string>({ name, message, importer, }: LoadComponentProps<K>) { - const [Component, setComponent] = useState<ProplessComponent | null>(null); + const [Component, setComponent] = useState<ProplessComponent | null>( + localModuleCache.get(name) ?? null, + ); const [failedToLoad, setFailedToLoad] = useState(false); useEffect(() => { + if (localModuleCache.has(name)) { + return; + } + setFailedToLoad(false); // Load the module; if it fails - retry with exponential backoff promiseRetry( retry => importer() - .then(module => setComponent(() => module[name])) + .then(module => { + const component = () => module[name]; + localModuleCache.set(name, component); + setComponent(component); + }) .catch(retry), { retries: 5, @@ -45,7 +61,7 @@ function LoadComponentInner<K extends string>({ }, [name, importer]); if (failedToLoad) { - throw new Error(`Failed loading the ${name} lazy module.`); + throw new LazyLoadFailedError(name); } if (!Component) { diff --git a/packages/loot-core/src/shared/errors.ts b/packages/loot-core/src/shared/errors.ts index c75424e12..e081481ff 100644 --- a/packages/loot-core/src/shared/errors.ts +++ b/packages/loot-core/src/shared/errors.ts @@ -91,3 +91,13 @@ export function getSyncError(error, id) { return `We had an unknown problem opening “${id}â€.`; } } + +export class LazyLoadFailedError extends Error { + type = 'app-init-failure'; + meta = {}; + + constructor(name: string) { + super(`Error: failed loading lazy-loaded module ${name}`); + this.meta = { name }; + } +} diff --git a/upcoming-release-notes/2657.md b/upcoming-release-notes/2657.md new file mode 100644 index 000000000..0c009975d --- /dev/null +++ b/upcoming-release-notes/2657.md @@ -0,0 +1,6 @@ +--- +category: Enhancements +authors: [MatissJanis] +--- + +Add custom error message if lazy-loading a module fails. -- GitLab