Skip to content

Commit 4416b15

Browse files
spivakevfulcanellee
authored andcommitted
fix(cdn-icon): fix icon update when name changes (#1558)
* fix(cdn-icon): fix icon update when name changes * refactor(cdn-icon): extract useIcon hook, add useSyncExternalStore hook * fix(cdn-icon): add core-components-shared dependency * chore(cdn-icon): fix quick-vans-return.md * chore(cdn-icon): fix quick-vans-return.md * fix: update dependencies --------- Co-authored-by: Elena Spivak <[email protected]> Co-authored-by: Mikhail Lukin <[email protected]> (cherry picked from commit 202249e)
1 parent 278e9c0 commit 4416b15

File tree

6 files changed

+108
-50
lines changed

6 files changed

+108
-50
lines changed

.changeset/odd-pens-sip.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@alfalab/core-components-cdn-icon': patch
3+
---
4+
5+
Рефакторинг `CDNIcon`: логика вынесена в хук `useIcon`, кеширование иконки переписано на `useSyncExternalStore`

.changeset/plenty-squids-smash.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@alfalab/core-components-cdn-icon': patch
3+
---
4+
5+
Исправлен баг `CDNIcon`, когда при изменении пропса `name` не менялась иконка

packages/cdn-icon/package.json

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@alfalab/core-components-cdn-icon",
3-
"version": "5.3.0",
3+
"version": "5.3.2",
44
"description": "",
55
"keywords": [],
66
"license": "ISC",
@@ -10,13 +10,15 @@
1010
"access": "public",
1111
"directory": "dist"
1212
},
13+
"sideEffects": false,
1314
"peerDependencies": {
1415
"react": "^16.9.0 || ^17.0.1 || ^18.0.0"
1516
},
1617
"dependencies": {
17-
"classnames": "^2.3.1",
18-
"tslib": "^2.4.0"
18+
"classnames": "^2.5.1",
19+
"tslib": "^2.4.0",
20+
"@alfalab/core-components-shared": "^0.16.0"
1921
},
20-
"themesVersion": "13.5.0",
21-
"varsVersion": "9.15.0"
22+
"themesVersion": "13.7.1",
23+
"varsVersion": "9.18.0"
2224
}

packages/cdn-icon/src/Component.tsx

Lines changed: 14 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
import React, { ReactNode, useEffect, useState } from 'react';
1+
import React, { ReactNode } from 'react';
22
import cn from 'classnames';
33

4+
import { LoadingStatus, useIcon } from './hooks/use-icon';
5+
46
import styles from './index.module.css';
57

68
type CDNIconProps = {
@@ -32,15 +34,6 @@ type CDNIconProps = {
3234
fallback?: ReactNode;
3335
};
3436

35-
enum LoadingStatus {
36-
INITIAL,
37-
SUCCESS,
38-
FAILURE,
39-
}
40-
41-
// Кэшируем загруженные иконки, чтобы предотвратить их повторную загрузку при каждом монтировании
42-
const cache: Record<string, string> = {};
43-
4437
export const CDNIcon: React.FC<CDNIconProps> = ({
4538
name,
4639
color,
@@ -49,48 +42,24 @@ export const CDNIcon: React.FC<CDNIconProps> = ({
4942
baseUrl = 'https://alfabank.servicecdn.ru/icons',
5043
fallback,
5144
}) => {
52-
const url = `${baseUrl}/${name}.svg`;
53-
54-
const [loadingStatus, setLoadingStatus] = useState<LoadingStatus>(LoadingStatus.INITIAL);
55-
const [icon, setIcon] = useState(cache[url]);
56-
57-
const monoIcon = !name.includes('_color');
58-
59-
useEffect(() => {
60-
if (icon) return undefined;
61-
62-
const xhr = new XMLHttpRequest();
45+
const [icon, status] = useIcon(`${baseUrl}/${name}.svg`);
6346

64-
xhr.open('GET', url);
65-
xhr.send();
66-
xhr.onload = function onload() {
67-
setLoadingStatus(LoadingStatus.SUCCESS);
68-
const svg = xhr.response;
69-
70-
if (svg.startsWith('<svg')) {
71-
cache[url] = svg;
72-
73-
setIcon(svg);
74-
}
75-
};
76-
77-
xhr.onerror = function onError() {
78-
setLoadingStatus(LoadingStatus.FAILURE);
79-
};
80-
81-
return () => xhr.abort();
82-
}, [url, icon]);
47+
const isMonoIcon = !name.includes('_color');
8348

8449
return (
8550
<span
8651
style={{ color }}
8752
className={cn('cc-cdn-icon', styles.component, className, {
88-
[styles.parentColor]: monoIcon,
53+
[styles.parentColor]: isMonoIcon,
8954
})}
9055
data-test-id={dataTestId}
91-
{...(loadingStatus === LoadingStatus.FAILURE
92-
? { children: fallback }
93-
: { dangerouslySetInnerHTML: { __html: icon } })}
94-
/>
56+
// eslint-disable-next-line react/no-danger
57+
dangerouslySetInnerHTML={
58+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
59+
status === LoadingStatus.SUCCESS ? { __html: icon! } : undefined
60+
}
61+
>
62+
{status === LoadingStatus.FAILURE ? fallback : undefined}
63+
</span>
9564
);
9665
};
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { useEffect, useState, useSyncExternalStore } from 'react';
2+
3+
import { hasOwnProperty, noop } from '@alfalab/core-components-shared';
4+
5+
type Listener = () => void;
6+
7+
export enum LoadingStatus {
8+
INITIAL,
9+
SUCCESS,
10+
FAILURE,
11+
}
12+
13+
// Кэшируем загруженные иконки, чтобы предотвратить их повторную загрузку при каждом монтировании
14+
let cache: Record<string, string | undefined> = {};
15+
let listeners: Listener[] = [];
16+
17+
const iconsStore = {
18+
set(url: string, icon: string) {
19+
cache = { ...cache, [url]: icon };
20+
21+
listeners.forEach((listener) => {
22+
listener();
23+
});
24+
},
25+
has(url: string) {
26+
return hasOwnProperty(cache, url);
27+
},
28+
subscribe(listener: Listener) {
29+
listeners = [...listeners, listener];
30+
31+
return () => {
32+
listeners = listeners.filter((l) => l !== listener);
33+
};
34+
},
35+
getSnapshot() {
36+
return cache;
37+
},
38+
};
39+
40+
export function useIcon(url: string): [icon: string | undefined, status: LoadingStatus] {
41+
const icons = useSyncExternalStore(iconsStore.subscribe, iconsStore.getSnapshot);
42+
const [loadingStatus, setLoadingStatus] = useState(LoadingStatus.INITIAL);
43+
44+
useEffect(() => {
45+
if (iconsStore.has(url)) {
46+
setLoadingStatus(LoadingStatus.SUCCESS);
47+
48+
return noop;
49+
}
50+
51+
setLoadingStatus(LoadingStatus.INITIAL);
52+
53+
const xhr = new XMLHttpRequest();
54+
55+
xhr.open('GET', url);
56+
xhr.send();
57+
xhr.onload = function onload() {
58+
const svg = xhr.response;
59+
60+
if (typeof svg === 'string' && svg.startsWith('<svg')) {
61+
iconsStore.set(url, svg);
62+
}
63+
setLoadingStatus(LoadingStatus.SUCCESS);
64+
};
65+
66+
xhr.onerror = function onError() {
67+
setLoadingStatus(LoadingStatus.FAILURE);
68+
};
69+
70+
return () => xhr.abort();
71+
}, [url]);
72+
73+
return [icons[url], loadingStatus];
74+
}

packages/cdn-icon/tsconfig.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,7 @@
99
"@alfalab/core-components-*": ["../*/src"]
1010
}
1111
},
12+
"references": [
13+
{ "path": "../shared" }
14+
]
1215
}

0 commit comments

Comments
 (0)