React组件中使用Children.map替换SVG为加载图标时偶尔失效的问题求助
大家好,我最近在开发一个通用的确认操作按钮组件,想实现点击后把shadcn Button里的SVG图标替换成lucid-react的LoaderIcon来展示加载状态。本来预期是点击按钮触发确认后,图标会变成旋转的加载器,但实际用下来发现有时候能正常替换,有时候完全没反应。
后来我排查了一下,发现失效的时候child.type根本不是我判断的"svg",而是一个类似ForwardRef(TrashIcon)的封装组件。
先给大家看看我当前的组件代码:
export function ConfirmedActionButton<T>({ action, actionParams, children, confirmOptions = {}, onSuccess, ...props }: ButtonProps & { action: (data: T) => Promise<FormActionState> actionParams: T confirmOptions?: ConfirmOptions onSuccess?: () => void }) { const [isPending, startTransition] = useTransition() const onConfirm = async () => { const { ok } = await safeConfirm(confirmOptions) if (ok) { startTransition(async () => { const res = await action(actionParams) if (res?.error) { toast.error("Une erreur est survenue.", { description: res.error }) } if (res?.success) { toast.success(res?.success) onSuccess?.() } }) } } const filteredChildren = useMemo( () => Children.map(children, (child) => { if (isValidElement(child) && child.type === "svg") { return isPending ? <Loader2Icon className="animate-spin" /> : child } return child }), [children, isPending] ) return ( <Button {...props} onClick={onConfirm} disabled={isPending}> {filteredChildren} </Button> ) }
组件的使用示例是这样的:
<ConfirmedActionButton className="absolute top-1.5 right-1.5 text-destructive rounded-full p-1" size="icon" variant="ghost" action={deleteAction} actionParams={asset.id} > <span className="sr-only">Delete asset</span> <TrashIcon className="duration-200 ease-in-out" /> </ConfirmedActionButton>
问题很明显了:我写的判断条件child.type === "svg"只能匹配原生的<svg>标签,但项目里用的TrashIcon这类图标其实是封装好的React函数组件(可能还被forwardRef包裹过),所以这个判断就直接跳过了,自然不会替换成加载图标。
给大家分享几个可行的解决方案:
方案1:给自定义图标组件添加标识属性
如果这些图标是你们自己维护的,可以给每个图标组件加一个静态属性,比如:
// TrashIcon.tsx export const TrashIcon = forwardRef((props, ref) => { // ...组件内容 }) TrashIcon.isIconComponent = true;
然后修改判断逻辑:
const filteredChildren = useMemo( () => Children.map(children, (child) => { if (isValidElement(child)) { const isIcon = child.type === "svg" || (child.type as any).isIconComponent; if (isIcon) { return isPending ? <Loader2Icon className="animate-spin" /> : child; } } return child; }), [children, isPending] );
这种方式最可靠,不会误判其他组件。
方案2:通过组件名称/displayName判断
如果没法修改图标组件的代码,可以尝试通过组件的displayName或者name来判断,比如大部分图标组件的名称都会以Icon结尾:
const filteredChildren = useMemo( () => Children.map(children, (child) => { if (isValidElement(child)) { const isNativeSvg = child.type === "svg"; const isCustomIcon = typeof child.type === "function" && (child.type.displayName?.endsWith("Icon") || child.type.name?.endsWith("Icon")); if (isNativeSvg || isCustomIcon) { return isPending ? <Loader2Icon className="animate-spin" /> : child; } } return child; }), [children, isPending] );
这个方式比较快捷,但要注意如果有其他非图标组件的名称也以Icon结尾,就会出现误判。
方案3:把图标单独作为props传入
这种方式更可控,直接把图标从children里抽出来作为单独的props,这样不用遍历children去判断:
export function ConfirmedActionButton<T>({ action, actionParams, children, icon, // 新增图标props confirmOptions = {}, onSuccess, ...props }: ButtonProps & { action: (data: T) => Promise<FormActionState> actionParams: T confirmOptions?: ConfirmOptions onSuccess?: () => void icon?: React.ReactNode }) { const [isPending, startTransition] = useTransition() // ...其他逻辑不变 const renderIcon = isPending ? <Loader2Icon className="animate-spin" /> : icon; return ( <Button {...props} onClick={onConfirm} disabled={isPending}> {children} {renderIcon} </Button> ) }
使用的时候改成:
<ConfirmedActionButton className="absolute top-1.5 right-1.5 text-destructive rounded-full p-1" size="icon" variant="ghost" action={deleteAction} actionParams={asset.id} icon={<TrashIcon className="duration-200 ease-in-out" />} > <span className="sr-only">Delete asset</span> </ConfirmedActionButton>
这种方式代码更清晰,也不会有判断错误的问题,推荐优先考虑。
希望这些方案能帮到遇到同样问题的朋友~
备注:内容来源于stack exchange,提问作者Benjamin Fourgeaud

