From f852ce70e7b98c35f3a31e6981d5135b4ae50c8e Mon Sep 17 00:00:00 2001 From: schroda <50052685+schroda@users.noreply.github.com> Date: Fri, 1 Mar 2024 01:17:23 +0100 Subject: [PATCH] Abort image requests on unmount (#624) --- src/components/util/SpinnerImage.tsx | 56 ++++++++++++++++++++++------ src/lib/requests/RequestManager.ts | 12 ++++++ 2 files changed, 56 insertions(+), 12 deletions(-) diff --git a/src/components/util/SpinnerImage.tsx b/src/components/util/SpinnerImage.tsx index f87f408a..015ce35c 100644 --- a/src/components/util/SpinnerImage.tsx +++ b/src/components/util/SpinnerImage.tsx @@ -14,6 +14,9 @@ import { Theme, SxProps, Stack, Button } from '@mui/material'; import BrokenImageIcon from '@mui/icons-material/BrokenImage'; import RefreshIcon from '@mui/icons-material/Refresh'; import { useTranslation } from 'react-i18next'; +import { CanceledError } from 'axios'; +import { requestManager } from '@/lib/requests/RequestManager.ts'; +import { defaultPromiseErrorHandler } from '@/util/defaultPromiseErrorHandler.ts'; interface IProps { src: string; @@ -32,6 +35,7 @@ export function SpinnerImage(props: IProps) { const { t } = useTranslation(); + const [imageSourceUrl, setImageSourceUrl] = useState(''); const [imgLoadRetryKey, setImgLoadRetryKey] = useState(0); const [isLoading, setIsLoading] = useState(undefined); const [hasError, setHasError] = useState(false); @@ -46,11 +50,39 @@ export function SpinnerImage(props: IProps) { }; useEffect(() => { - // only activate the loading state in case the image has not been cached yet. - // otherwise, the loading placeholder will always be visible before the actual image is shown, which looks like flickering - const timeout = setTimeout(() => setIsLoading((prevState) => (prevState === undefined ? true : prevState)), 1); - return () => clearTimeout(timeout); - }, []); + const imageRequest = requestManager.requestImage(src); + + const fetchImage = async () => { + try { + const updateImage = async () => { + const image = await imageRequest.response; + + updateImageState(false); + setImageSourceUrl(image); + }; + + const checkCache = await Promise.race([imageRequest.response, Promise.resolve(false)]); + const isImageCached = !!checkCache; + + if (isImageCached) { + await updateImage(); + return; + } + + updateImageState(true); + await updateImage(); + } catch (e) { + const wasAborted = e instanceof CanceledError; + updateImageState(false, !wasAborted); + } + }; + + fetchImage().catch(defaultPromiseErrorHandler); + + return () => { + imageRequest.abortRequest(new Error('Component was unmounted')); + }; + }, [imgLoadRetryKey]); return ( <> @@ -58,7 +90,7 @@ export function SpinnerImage(props: IProps) { {isLoading && } - {hasError && ( + {hasError && isLoading === false && ( <>