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:
Mantas Kantautas
2026-10-27 07:45:01 +01:00
committed by GitHub
parent 03f11e2d6a
commit e8f86fc2d2
2 changed files with 62 additions and 2 deletions
+11 -1
View File
@@ -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');
});
});