mirror of
https://github.com/lucide-icons/lucide.git
synced 2026-11-13 13:48:26 +00:00
refactor(lucide-react): use buildLucideIconForReact again in Icon
- Icon: drop the module-level icon cache and call the shared builder again; only the child elements are memoized with useMemo - shared: restore the original mergeClasses and buildLucideIconNode Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G9u8Fgx7hqgYx7gfUQgfY6
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { createElement, forwardRef, type ReactElement } from 'react';
|
||||
import { defaultReactAttributes, hasA11yProp, mergeClasses } from '@lucide/shared';
|
||||
import { createElement, forwardRef, useMemo, type ReactElement } from 'react';
|
||||
import { buildLucideIconForReact, hasA11yProp, mergeClasses } from '@lucide/shared';
|
||||
import { LucideIconData, LucideIconNode, LucideProps } from './types';
|
||||
import { useLucideContext } from './context';
|
||||
|
||||
@@ -17,79 +17,10 @@ type IconComponentProps = LucideProps &
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Everything about an icon that doesn't depend on the props it is rendered with.
|
||||
*/
|
||||
type StaticIcon = {
|
||||
viewBox: string;
|
||||
classNames: string[];
|
||||
className: string;
|
||||
children: ReactElement[];
|
||||
nonScalingChildren?: ReactElement[];
|
||||
};
|
||||
|
||||
const DEFAULT_SIZE = defaultReactAttributes.width;
|
||||
const EMPTY_ICON_NODE: LucideIconNode[] = [];
|
||||
const NON_SCALING_STROKE = { vectorEffect: 'non-scaling-stroke' } as const;
|
||||
|
||||
const staticIcons = new WeakMap<LucideIconData, StaticIcon>();
|
||||
const legacyIcons = new WeakMap<LucideIconNode[], LucideIconData>();
|
||||
|
||||
const createNodeElement = (
|
||||
[tag, attrs, children]: LucideIconNode,
|
||||
index: number,
|
||||
extraAttrs?: object,
|
||||
): ReactElement =>
|
||||
createElement(
|
||||
tag,
|
||||
// The generated icons carry a key per node, custom icon nodes may not.
|
||||
{ key: index, ...extraAttrs, ...attrs },
|
||||
children?.map((child, childIndex) => createNodeElement(child, childIndex)),
|
||||
);
|
||||
|
||||
/**
|
||||
* Icon data is static, so the class names, the view box and the child elements are created once
|
||||
* per icon instead of on every render. Handing React the very same child elements again also lets
|
||||
* it skip them when an icon re-renders.
|
||||
*/
|
||||
const getStaticIcon = (icon: LucideIconData): StaticIcon => {
|
||||
let staticIcon = staticIcons.get(icon);
|
||||
|
||||
if (staticIcon == null) {
|
||||
const classNames = ['lucide'];
|
||||
|
||||
if (icon.name) {
|
||||
classNames.push(`lucide-${icon.name}`);
|
||||
}
|
||||
|
||||
for (const alias of icon.aliases ?? []) {
|
||||
if (typeof alias === 'string' && alias.trim() !== '') {
|
||||
classNames.push(`lucide-${alias}`);
|
||||
}
|
||||
}
|
||||
|
||||
staticIcon = {
|
||||
viewBox: `0 0 ${icon.size ?? icon.width ?? DEFAULT_SIZE} ${icon.size ?? icon.height ?? DEFAULT_SIZE}`,
|
||||
classNames,
|
||||
className: mergeClasses(...classNames),
|
||||
children: icon.node.map((node, index) => createNodeElement(node, index)),
|
||||
};
|
||||
staticIcons.set(icon, staticIcon);
|
||||
}
|
||||
|
||||
return staticIcon;
|
||||
};
|
||||
|
||||
const getLegacyIcon = (iconNode: LucideIconNode[]): LucideIconData => {
|
||||
let icon = legacyIcons.get(iconNode);
|
||||
|
||||
if (icon == null) {
|
||||
icon = { node: iconNode, size: DEFAULT_SIZE };
|
||||
legacyIcons.set(iconNode, icon);
|
||||
}
|
||||
|
||||
return icon;
|
||||
};
|
||||
const renderNode = ([tag, attrs, children]: LucideIconNode): ReactElement =>
|
||||
createElement(tag, attrs, children?.map(renderNode));
|
||||
|
||||
/**
|
||||
* Lucide icon component
|
||||
@@ -117,57 +48,50 @@ const Icon = forwardRef<SVGSVGElement, IconComponentProps>(
|
||||
strokeWidth,
|
||||
absoluteStrokeWidth,
|
||||
nonScalingStroke,
|
||||
className,
|
||||
className = '',
|
||||
children,
|
||||
iconNode = EMPTY_ICON_NODE,
|
||||
icon = getLegacyIcon(iconNode),
|
||||
icon = {
|
||||
node: iconNode,
|
||||
aliases: [],
|
||||
size: 24,
|
||||
},
|
||||
...rest
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const context = useLucideContext();
|
||||
const staticIcon = getStaticIcon(icon);
|
||||
const {
|
||||
size: contextSize = 24,
|
||||
strokeWidth: contextStrokeWidth = 2,
|
||||
absoluteStrokeWidth: contextAbsoluteStrokeWidth = false,
|
||||
nonScalingStroke: contextNonScalingStroke = false,
|
||||
color: contextColor = 'currentColor',
|
||||
className: contextClass = '',
|
||||
} = useLucideContext() ?? {};
|
||||
|
||||
const resolvedWidth = width ?? size ?? context.size ?? DEFAULT_SIZE;
|
||||
const resolvedStrokeWidth =
|
||||
strokeWidth ?? context.strokeWidth ?? defaultReactAttributes.strokeWidth;
|
||||
const contextClassName = context.className;
|
||||
const hasAccessibleProp = Boolean(children) || hasA11yProp(rest);
|
||||
const resolvedNonScalingStroke = nonScalingStroke ?? contextNonScalingStroke;
|
||||
|
||||
let iconChildren = staticIcon.children;
|
||||
const [name, svgAttributes, builtIconNode = []] = buildLucideIconForReact(icon, {
|
||||
color: color ?? contextColor,
|
||||
width: width ?? size ?? contextSize,
|
||||
height: height ?? size ?? contextSize,
|
||||
strokeWidth: strokeWidth ?? contextStrokeWidth,
|
||||
absoluteStrokeWidth: absoluteStrokeWidth ?? contextAbsoluteStrokeWidth,
|
||||
nonScalingStroke: resolvedNonScalingStroke,
|
||||
className: mergeClasses(contextClass, className),
|
||||
hasA11yProp: hasAccessibleProp,
|
||||
attributes: rest,
|
||||
});
|
||||
|
||||
if (nonScalingStroke ?? context.nonScalingStroke) {
|
||||
iconChildren = staticIcon.nonScalingChildren ??= icon.node.map((node, index) =>
|
||||
createNodeElement(node, index, NON_SCALING_STROKE),
|
||||
);
|
||||
}
|
||||
|
||||
return createElement(
|
||||
'svg',
|
||||
{
|
||||
ref,
|
||||
...defaultReactAttributes,
|
||||
width: resolvedWidth,
|
||||
height: height ?? size ?? context.size ?? DEFAULT_SIZE,
|
||||
viewBox: staticIcon.viewBox,
|
||||
stroke: (color ?? context.color) || defaultReactAttributes.stroke,
|
||||
strokeWidth:
|
||||
(absoluteStrokeWidth ?? context.absoluteStrokeWidth)
|
||||
? (Number(resolvedStrokeWidth) * Number(icon.size ?? icon.width ?? DEFAULT_SIZE)) /
|
||||
Number(resolvedWidth)
|
||||
: resolvedStrokeWidth,
|
||||
className:
|
||||
className || contextClassName
|
||||
? mergeClasses(
|
||||
...staticIcon.classNames,
|
||||
...`${contextClassName ?? ''} ${className ?? ''}`.split(' '),
|
||||
)
|
||||
: staticIcon.className,
|
||||
...(!children && !hasA11yProp(rest) && { 'aria-hidden': 'true' }),
|
||||
...rest,
|
||||
},
|
||||
iconChildren,
|
||||
children,
|
||||
// The child elements only depend on the icon nodes and the non scaling stroke setting.
|
||||
// Handing React the same elements again lets it skip them when the icon re-renders.
|
||||
const iconChildren = useMemo(
|
||||
() => builtIconNode.map(renderNode),
|
||||
[icon.node, resolvedNonScalingStroke],
|
||||
);
|
||||
|
||||
return createElement(name, { ref, ...svgAttributes }, iconChildren, children);
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -13,29 +13,22 @@ function isDefined<T>(value: T | null | undefined): value is T {
|
||||
* @param params Additional build parameters.
|
||||
*/
|
||||
function buildLucideIconNode(icon: LucideIconData, params: LucideBuildParams = {}): LucideIconNode {
|
||||
const attributeNames = params.attributeNames;
|
||||
const attributeNames = params.attributeNames ?? {};
|
||||
const getAttributeName = (attributeName: string) =>
|
||||
attributeNames?.[attributeName] ?? attributeName;
|
||||
attributeNames[attributeName] ?? attributeName;
|
||||
|
||||
const viewBoxWidth = icon.size ?? icon.width ?? defaultAttributes['width'];
|
||||
const viewBoxHeight = icon.size ?? icon.height ?? defaultAttributes['height'];
|
||||
|
||||
const classNames: string[] = [];
|
||||
if (params.includeDefaultClasses !== false) {
|
||||
classNames.push('lucide');
|
||||
if (icon.name) {
|
||||
classNames.push(`lucide-${icon.name}`);
|
||||
}
|
||||
for (const alias of icon.aliases ?? []) {
|
||||
if (typeof alias === 'string' && alias.trim() !== '') {
|
||||
classNames.push(`lucide-${alias}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (params.className) {
|
||||
classNames.push(...params.className.split(' '));
|
||||
}
|
||||
const className = mergeClasses(...classNames);
|
||||
const aliasClassNames =
|
||||
icon.aliases
|
||||
?.filter((alias: string): alias is string => typeof alias === 'string' && alias.trim() !== '')
|
||||
.map((alias: string) => `lucide-${alias}`) ?? [];
|
||||
const iconClassNames = [...(icon.name ? [`lucide-${icon.name}`] : []), ...aliasClassNames];
|
||||
const classNamesFromClassName = params.className?.split(' ').filter(Boolean) ?? [];
|
||||
const className =
|
||||
params.includeDefaultClasses === false
|
||||
? mergeClasses(...classNamesFromClassName)
|
||||
: mergeClasses('lucide', ...iconClassNames, ...classNamesFromClassName);
|
||||
|
||||
const calculatedStrokeWidth = params.absoluteStrokeWidth
|
||||
? (Number(params.strokeWidth ?? defaultAttributes['stroke-width']) *
|
||||
@@ -43,48 +36,48 @@ function buildLucideIconNode(icon: LucideIconData, params: LucideBuildParams = {
|
||||
Number(params.size ?? params.width ?? defaultAttributes['width'])
|
||||
: (params.strokeWidth ?? defaultAttributes['stroke-width']);
|
||||
|
||||
// Keys are assigned in a fixed order, so the attributes are always emitted in the same order.
|
||||
const attributes: SVGProps = {};
|
||||
|
||||
for (const attributeName in defaultAttributes) {
|
||||
attributes[getAttributeName(attributeName)] =
|
||||
defaultAttributes[attributeName as keyof typeof defaultAttributes];
|
||||
}
|
||||
|
||||
if (params.color) {
|
||||
attributes[getAttributeName('stroke')] = params.color;
|
||||
}
|
||||
if (isDefined(params.size)) {
|
||||
attributes[getAttributeName('width')] = params.size;
|
||||
attributes[getAttributeName('height')] = params.size;
|
||||
}
|
||||
if (isDefined(params.width)) {
|
||||
attributes[getAttributeName('width')] = params.width;
|
||||
}
|
||||
if (isDefined(params.height)) {
|
||||
attributes[getAttributeName('height')] = params.height;
|
||||
}
|
||||
attributes[getAttributeName('stroke-width')] = calculatedStrokeWidth;
|
||||
if (className) {
|
||||
attributes[getAttributeName('class')] = className;
|
||||
}
|
||||
attributes[getAttributeName('viewBox')] = `0 0 ${viewBoxWidth} ${viewBoxHeight}`;
|
||||
if (params.hasA11yProp === false) {
|
||||
attributes[getAttributeName('aria-hidden')] = 'true';
|
||||
}
|
||||
Object.assign(attributes, params.attributes);
|
||||
|
||||
const nonScalingStrokeAttributes = params.nonScalingStroke
|
||||
? { [getAttributeName('vector-effect')]: 'non-scaling-stroke' }
|
||||
: null;
|
||||
const attributes = {
|
||||
...Object.entries(defaultAttributes).reduce((attrs, [attrName, value]) => {
|
||||
attrs[getAttributeName(attrName)] = value;
|
||||
return attrs;
|
||||
}, {} as SVGProps),
|
||||
...('color' in params &&
|
||||
params.color && {
|
||||
[getAttributeName('stroke')]: params.color,
|
||||
}),
|
||||
...('size' in params &&
|
||||
isDefined(params.size) && {
|
||||
[getAttributeName('width')]: params.size,
|
||||
[getAttributeName('height')]: params.size,
|
||||
}),
|
||||
...('width' in params &&
|
||||
isDefined(params.width) && {
|
||||
[getAttributeName('width')]: params.width,
|
||||
}),
|
||||
...('height' in params &&
|
||||
isDefined(params.height) && {
|
||||
[getAttributeName('height')]: params.height,
|
||||
}),
|
||||
[getAttributeName('stroke-width')]: calculatedStrokeWidth,
|
||||
...(className && {
|
||||
[getAttributeName('class')]: className,
|
||||
}),
|
||||
[getAttributeName('viewBox')]: `0 0 ${viewBoxWidth} ${viewBoxHeight}`,
|
||||
...(params.hasA11yProp === false
|
||||
? {
|
||||
[getAttributeName('aria-hidden')]: 'true',
|
||||
}
|
||||
: {}),
|
||||
...('attributes' in params && params.attributes),
|
||||
};
|
||||
|
||||
return [
|
||||
'svg',
|
||||
attributes,
|
||||
icon.node.map((child): LucideIconNode => {
|
||||
const [name, attrs, children] = child;
|
||||
const nextAttrs = nonScalingStrokeAttributes
|
||||
? { ...nonScalingStrokeAttributes, ...attrs }
|
||||
const nextAttrs = params.nonScalingStroke
|
||||
? { [getAttributeName('vector-effect')]: 'non-scaling-stroke', ...attrs }
|
||||
: attrs;
|
||||
|
||||
return children ? [name, nextAttrs, children] : [name, nextAttrs];
|
||||
|
||||
@@ -4,18 +4,14 @@
|
||||
* @param {array} classes
|
||||
* @returns {string} A string of classes
|
||||
*/
|
||||
export const mergeClasses = <ClassType = string | undefined | null>(...classes: ClassType[]) => {
|
||||
const uniqueClasses: ClassType[] = [];
|
||||
|
||||
for (const className of classes) {
|
||||
if (
|
||||
Boolean(className) &&
|
||||
(className as string).trim() !== '' &&
|
||||
!uniqueClasses.includes(className)
|
||||
) {
|
||||
uniqueClasses.push(className);
|
||||
}
|
||||
}
|
||||
|
||||
return uniqueClasses.join(' ').trim();
|
||||
};
|
||||
export const mergeClasses = <ClassType = string | undefined | null>(...classes: ClassType[]) =>
|
||||
classes
|
||||
.filter((className, index, array) => {
|
||||
return (
|
||||
Boolean(className) &&
|
||||
(className as string).trim() !== '' &&
|
||||
array.indexOf(className) === index
|
||||
);
|
||||
})
|
||||
.join(' ')
|
||||
.trim();
|
||||
|
||||
Reference in New Issue
Block a user