-
Notifications
You must be signed in to change notification settings - Fork 52
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix(cdn-icon): fix icon update when name changes #1558
base: master
Are you sure you want to change the base?
Conversation
🦋 Changeset detectedLatest commit: 0ef81d7 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Pull Request Test Coverage Report for Build 13201798830Details
💛 - Coveralls |
Собрана новая демка. |
Собрана новая демка. |
Собрана новая демка. |
@@ -53,11 +53,18 @@ export const CDNIcon: React.FC<CDNIconProps> = ({ | |||
|
|||
const [loadingStatus, setLoadingStatus] = useState<LoadingStatus>(LoadingStatus.INITIAL); | |||
const [icon, setIcon] = useState(cache[url]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
здесь комплексная проблема
icon
должен быть derived (computed) state изcache
иurl
, то есть не хранится отдельно- в таком случае
cache
должно стать состоянием и тут на помощь приходит useSyncExternalStore
поэтому предлагаю сделать обертку use-cache.ts
по аналогии с todosStore.js
из ссылки выше
import { useSyncExternalStore } from 'react';
type Listener = () => void;
// Кэшируем загруженные иконки, чтобы предотвратить их повторную загрузку при каждом монтировании
let cache: Record<string, string> = {};
let listeners: Listener[] = [];
const cacheStore = {
set(url: string, icon: string) {
cache = { ...cache, [url]: icon };
listeners.forEach((listener) => {
listener();
});
},
subscribe(listener: Listener) {
listeners = [...listeners, listener];
return () => {
listeners = listeners.filter((l) => l !== listener);
};
},
getSnapshot() {
return cache;
},
};
export function useCache() {
return [
useSyncExternalStore(cacheStore.subscribe, cacheStore.getSnapshot),
cacheStore.set,
] as const;
}
и соответствующе исправить Component.tsx
import React, { ReactNode, useEffect, useState } from 'react';
import cn from 'classnames';
import { useCache } from './use-cache';
import styles from './index.module.css';
type CDNIconProps = {
/**
* Имя иконки
*/
name: string;
/**
* Цвет иконки
*/
color?: string;
/**
* Дополнительный класс
*/
className?: string;
/**
* Базовый адрес cdn хранилища c иконками
* @default https://alfabank.servicecdn.ru/icons
*/
baseUrl?: string;
/**
* Идентификатор для систем автоматизированного тестирования
*/
dataTestId?: string;
/**
* Fallback на случай, если не удастся загрузить иконку
*/
fallback?: ReactNode;
};
enum LoadingStatus {
INITIAL,
SUCCESS,
FAILURE,
}
export const CDNIcon: React.FC<CDNIconProps> = ({
name,
color,
dataTestId,
className,
baseUrl = 'https://alfabank.servicecdn.ru/icons',
fallback,
}) => {
const url = `${baseUrl}/${name}.svg`;
const [loadingStatus, setLoadingStatus] = useState<LoadingStatus>(LoadingStatus.INITIAL);
const [cache, setIcon] = useCache();
const icon = cache[url];
const monoIcon = !name.includes('_color');
useEffect(() => {
if (icon) return undefined;
const xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.send();
xhr.onload = function onload() {
setLoadingStatus(LoadingStatus.SUCCESS);
const svg = xhr.response;
if (svg.startsWith('<svg')) {
setIcon(url, svg);
}
};
xhr.onerror = function onError() {
setLoadingStatus(LoadingStatus.FAILURE);
};
return () => xhr.abort();
}, [url, icon, setIcon]);
return (
<span
style={{ color }}
className={cn('cc-cdn-icon', styles.component, className, {
[styles.parentColor]: monoIcon,
})}
data-test-id={dataTestId}
{...(loadingStatus === LoadingStatus.FAILURE
? { children: fallback }
: { dangerouslySetInnerHTML: { __html: icon } })}
/>
);
};
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
немного подправил код)
use-icon.ts
import { useEffect, useState, useSyncExternalStore } from 'react';
import { hasOwnProperty, noop } from '@alfalab/core-components-shared';
type Listener = () => void;
export enum LoadingStatus {
INITIAL,
SUCCESS,
FAILURE,
}
// Кэшируем загруженные иконки, чтобы предотвратить их повторную загрузку при каждом монтировании
let cache: Record<string, string | undefined> = {};
let listeners: Listener[] = [];
const iconsStore = {
set(url: string, icon: string) {
cache = { ...cache, [url]: icon };
listeners.forEach((listener) => {
listener();
});
},
has(url: string) {
return hasOwnProperty(cache, url);
},
subscribe(listener: Listener) {
listeners = [...listeners, listener];
return () => {
listeners = listeners.filter((l) => l !== listener);
};
},
getSnapshot() {
return cache;
},
};
export function useIcon(url: string): [icon: string | undefined, status: LoadingStatus] {
const icons = useSyncExternalStore(iconsStore.subscribe, iconsStore.getSnapshot);
const [loadingStatus, setLoadingStatus] = useState(LoadingStatus.INITIAL);
useEffect(() => {
if (iconsStore.has(url)) {
setLoadingStatus(LoadingStatus.SUCCESS);
return noop;
}
setLoadingStatus(LoadingStatus.INITIAL);
const xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.send();
xhr.onload = function onload() {
const svg = xhr.response;
if (typeof svg === 'string' && svg.startsWith('<svg')) {
iconsStore.set(url, svg);
}
setLoadingStatus(LoadingStatus.SUCCESS);
};
xhr.onerror = function onError() {
setLoadingStatus(LoadingStatus.FAILURE);
};
return () => xhr.abort();
}, [url]);
return [icons[url], loadingStatus];
}
и код компонента Component.tsx
import React, { ReactNode } from 'react';
import cn from 'classnames';
import { LoadingStatus, useIcon } from './use-icon';
import styles from './index.module.css';
type CDNIconProps = {
/**
* Имя иконки
*/
name: string;
/**
* Цвет иконки
*/
color?: string;
/**
* Дополнительный класс
*/
className?: string;
/**
* Базовый адрес cdn хранилища c иконками
* @default https://alfabank.servicecdn.ru/icons
*/
baseUrl?: string;
/**
* Идентификатор для систем автоматизированного тестирования
*/
dataTestId?: string;
/**
* Fallback на случай, если не удастся загрузить иконку
*/
fallback?: ReactNode;
};
export const CDNIcon: React.FC<CDNIconProps> = ({
name,
color,
dataTestId,
className,
baseUrl = 'https://alfabank.servicecdn.ru/icons',
fallback,
}) => {
const [icon, status] = useIcon(`${baseUrl}/${name}.svg`);
const monoIcon = !name.includes('_color');
return (
<span
style={{ color }}
className={cn('cc-cdn-icon', styles.component, className, {
[styles.parentColor]: monoIcon,
})}
data-test-id={dataTestId}
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
status === LoadingStatus.SUCCESS ? { __html: icon! } : undefined
}
>
{status === LoadingStatus.FAILURE ? fallback : undefined}
</span>
);
};
Опишите проблему
При изменении пропса
name
CDNIсon не перерисовывает иконкуШаги для воспроизведения
Ожидаемое поведение