Skip to content

Commit bb21045

Browse files
authored
fix: page icon auth if needed (#192)
1 parent 33a46b0 commit bb21045

File tree

2 files changed

+59
-6
lines changed

2 files changed

+59
-6
lines changed

src/components/_shared/view-icon/PageIcon.tsx

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { ReactComponent as CalendarSvg } from '@/assets/icons/calendar.svg';
88
import { ReactComponent as GridSvg } from '@/assets/icons/grid.svg';
99
import { ReactComponent as DocumentSvg } from '@/assets/icons/page.svg';
1010
import { cn } from '@/lib/utils';
11+
import { getImageUrl, revokeBlobUrl } from '@/utils/authenticated-image';
1112
import { renderColor } from '@/utils/color';
1213
import { getIcon, isFlagEmoji } from '@/utils/emoji';
1314

@@ -24,6 +25,7 @@ function PageIcon({
2425
iconSize?: number;
2526
}) {
2627
const [iconContent, setIconContent] = React.useState<string | undefined>(undefined);
28+
const [imgSrc, setImgSrc] = React.useState<string | undefined>(undefined);
2729

2830
const emoji = useMemo(() => {
2931
if (view.icon && view.icon.ty === ViewIconType.Emoji && view.icon.value) {
@@ -33,17 +35,36 @@ function PageIcon({
3335
return null;
3436
}, [view]);
3537

36-
const img = useMemo(() => {
38+
useEffect(() => {
39+
let currentBlobUrl: string | undefined;
40+
3741
if (view.icon && view.icon.ty === ViewIconType.URL && view.icon.value) {
42+
void getImageUrl(view.icon.value).then((url) => {
43+
currentBlobUrl = url;
44+
setImgSrc(url);
45+
});
46+
} else {
47+
setImgSrc(undefined);
48+
}
49+
50+
return () => {
51+
if (currentBlobUrl) {
52+
revokeBlobUrl(currentBlobUrl);
53+
}
54+
};
55+
}, [view.icon]);
56+
57+
const img = useMemo(() => {
58+
if (imgSrc) {
3859
return (
3960
<span className={cn('h-full w-full p-[2px]', className)}>
40-
<img className={'h-full w-full'} src={view.icon.value} alt='icon' />
61+
<img className={'h-full w-full'} src={imgSrc} alt='icon' />
4162
</span>
4263
);
4364
}
4465

4566
return null;
46-
}, [className, view.icon]);
67+
}, [className, imgSrc]);
4768

4869
const isFlag = useMemo(() => {
4970
return emoji ? isFlagEmoji(emoji) : false;

src/utils/authenticated-image.ts

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,10 +81,42 @@ export async function getImageUrl(url: string | undefined): Promise<string> {
8181
}
8282

8383
/**
84-
* Cleans up a blob URL created by fetchAuthenticatedImage
85-
* Should be called when the component unmounts or the URL is no longer needed
84+
* Cleans up a blob URL created by fetchAuthenticatedImage.
8685
*
87-
* @param url - The blob URL to revoke
86+
* ## Why this is needed
87+
*
88+
* When `fetchAuthenticatedImage` fetches an image with auth headers, it creates
89+
* a Blob URL using `URL.createObjectURL()`. This URL holds a reference to the
90+
* binary image data in browser memory.
91+
*
92+
* The browser keeps this data alive as long as the Blob URL exists - even if:
93+
* - The `<img>` element is removed from the DOM
94+
* - The React component unmounts
95+
* - The URL is no longer referenced anywhere in code
96+
*
97+
* Without calling `revokeBlobUrl`, each authenticated image fetch causes a
98+
* memory leak. For example, browsing 100 pages with 1MB icon images would
99+
* accumulate ~100MB in memory that is never freed until page reload.
100+
*
101+
* ## Usage
102+
*
103+
* Call this function in a useEffect cleanup when the component unmounts
104+
* or when the image URL changes:
105+
*
106+
* ```tsx
107+
* useEffect(() => {
108+
* let blobUrl: string | undefined;
109+
* getImageUrl(url).then((result) => {
110+
* blobUrl = result;
111+
* setImgSrc(result);
112+
* });
113+
* return () => {
114+
* if (blobUrl) revokeBlobUrl(blobUrl);
115+
* };
116+
* }, [url]);
117+
* ```
118+
*
119+
* @param url - The blob URL to revoke. Safe to call with non-blob URLs (no-op).
88120
*/
89121
export function revokeBlobUrl(url: string): void {
90122
if (url && url.startsWith('blob:')) {

0 commit comments

Comments
 (0)