You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React组件中使用Children.map替换SVG为加载图标时偶尔失效的问题求助

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.13 18:44:49