mirror of
https://github.com/lucide-icons/lucide.git
synced 2026-11-10 13:18:25 +00:00
fix(lucide-react): ignore stale DynamicIcon loads after the name changes (#5060)
When the name prop changes while the previous icon is still loading, the earlier import could resolve last and overwrite the newer icon, leaving DynamicIcon showing the wrong icon. The effect now ignores results from a superseded name.
This commit is contained in:
@@ -55,11 +55,21 @@ const DynamicIcon = forwardRef<SVGSVGElement, DynamicIconComponentProps>(
|
||||
const [iconData, setIconData] = useState<LucideIconData>();
|
||||
|
||||
useEffect(() => {
|
||||
let ignore = false;
|
||||
|
||||
getIconData(name)
|
||||
.then(setIconData)
|
||||
.then((data) => {
|
||||
if (!ignore) {
|
||||
setIconData(data);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
});
|
||||
|
||||
return () => {
|
||||
ignore = true;
|
||||
};
|
||||
}, [name]);
|
||||
|
||||
if (iconData == null) {
|
||||
|
||||
@@ -1,8 +1,32 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { act, render, waitFor, type RenderResult } from '@testing-library/react';
|
||||
|
||||
import DynamicIcon from '../src/DynamicIcon';
|
||||
|
||||
const { delayedImports, pendingImports } = vi.hoisted(() => ({
|
||||
delayedImports: new Map<string, Promise<void>>(),
|
||||
pendingImports: new Map<string, Promise<unknown>>(),
|
||||
}));
|
||||
|
||||
vi.mock('../src/dynamicIconImports', async (importOriginal) => {
|
||||
const { default: dynamicIconImports } =
|
||||
await importOriginal<typeof import('../src/dynamicIconImports')>();
|
||||
|
||||
return {
|
||||
default: Object.fromEntries(
|
||||
Object.entries(dynamicIconImports).map(([name, importIcon]) => [
|
||||
name,
|
||||
() => {
|
||||
const pendingImport = (delayedImports.get(name) ?? Promise.resolve()).then(importIcon);
|
||||
pendingImports.set(name, pendingImport);
|
||||
|
||||
return pendingImport;
|
||||
},
|
||||
]),
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
describe('Using DynamicIcon Component', () => {
|
||||
it('should render icon by given name', async () => {
|
||||
let container: RenderResult['container'];
|
||||
@@ -81,4 +105,30 @@ describe('Using DynamicIcon Component', () => {
|
||||
expect(ref.current).toBeInstanceOf(SVGSVGElement);
|
||||
});
|
||||
});
|
||||
|
||||
it('should render the latest icon when a previous icon finishes loading later', async () => {
|
||||
let resolveCameraImport = () => {};
|
||||
delayedImports.set(
|
||||
'camera',
|
||||
new Promise((resolve) => {
|
||||
resolveCameraImport = resolve;
|
||||
}),
|
||||
);
|
||||
|
||||
const { container, rerender } = render(<DynamicIcon name="camera" />);
|
||||
rerender(<DynamicIcon name="smile" />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(container.firstChild).toHaveClass('lucide-smile');
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
resolveCameraImport();
|
||||
await pendingImports.get('camera');
|
||||
await new Promise((resolve) => setTimeout(resolve));
|
||||
});
|
||||
|
||||
expect(container.firstChild).toHaveClass('lucide-smile');
|
||||
expect(container.firstChild).not.toHaveClass('lucide-camera');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user