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

TypeScript封装React组件:捕获事件、转发Ref与埋点实现

问题:封装MUI Button添加埋点并保留原组件完整行为

需求是给应用中所有MUI Button(原生HTML Button场景同理)统一添加分析埋点,计划通过Wrapper组件替代原Button,要求:

  • 封装时注入埋点逻辑
  • 完全保留原组件的事件监听与传播机制
  • 正确转发Ref

当前实现遇到两个核心问题:

  1. Ref类型混淆:搞不清ForwardRef、Ref、RefObject的差异
  2. 事件处理错误:尝试用dispatchEvent传递React SyntheticEvent时出错,不确定当前实现路径是否正确,想了解React推荐的「隐形」封装方式,确保不阻断原有函数与事件调用

当前实现代码

CustomButton.tsx

import _ from 'lodash';
import { Button, ButtonProps } from '@mui/material';
import { analyticsTrack, mergeRefs } from 'utils/functions';
import { forwardRef, ForwardedRef, createRef } from 'react';

interface CustomButtonProps extends ButtonProps {
  trackingName?: string;
}

const CustomButton = forwardRef(
  (
    { trackingName, children, ...rest }: CustomButtonProps,
    ref: ForwardedRef<any>
  ) => {
    const buttonRef = createRef();
    function handleClick(...args: any) {
      analyticsTrack('Button Clicked', {
        props: rest,
        ariaLabel: rest['aria-label'],
        trackingName,
        buttonText: typeof children === 'string' ? children : '',
      });
      if (rest.onClick) {
        rest.onClick(args);
      }
    }

    return (
      // @ts-ignore
      // eslint-disable-next-line react/jsx-no-bind
      <Button
        {..._.omitBy(rest, ['onClick'])}
        onMouseDown={(event) => event.stopPropagation()}
        onTouchStart={(event) => event.stopPropagation()}
        onClick={(event) => {
          event.stopPropagation();
          event.preventDefault();
          handleClick();
          // @ts-ignore
          buttonRef.current && buttonRef.current.dispatchEvent(event);
        }}
        // @ts-ignore
        ref={mergeRefs(ref, buttonRef)}
      >
        {children}
      </Button>
    );
  }
);

export default CustomButton;

mergeRefs 工具函数

export const mergeRefs = (...refs: RefObject<Element | null>[]) => {
  return ( node: Element ) => {
    for (const ref of refs) {
      // @ts-ignore
      ref && !ref.current ? ref.current = node : null;
    }
  }
}

解决方案

1. 先理清Ref相关类型差异

  • RefObject:通过createRef()创建的对象,包含current属性,用于存储DOM节点或组件实例,是可变引用
  • ForwardedRef:forwardRef接收的第二个参数,类型为Ref<T> | undefined,可以是RefObject或者回调函数((node: T) => void)
  • Ref:是RefObject<T>和回调Ref的联合类型,泛指所有可以用于React元素的引用类型

2. 修正后的「隐形」封装实现

React推荐的隐形封装核心是:不破坏原组件的API和行为,仅在原有逻辑上注入额外功能,不需要阻止事件传播或手动模拟事件派发。

首先修正mergeRefs函数(支持所有Ref类型)

import { Ref, RefObject } from 'react';

export const mergeRefs = <T>(...refs: Ref<T>[]) => {
  return (node: T | null) => {
    refs.forEach(ref => {
      if (!ref) return;
      // 处理回调类型的Ref
      if (typeof ref === 'function') {
        ref(node);
      } else {
        // 处理RefObject类型
        (ref as RefObject<T>).current = node;
      }
    });
  };
};

修正后的CustomButton组件

import { Button, ButtonProps } from '@mui/material';
import { analyticsTrack, mergeRefs } from 'utils/functions';
import { forwardRef, ForwardedRef } from 'react';

interface CustomButtonProps extends ButtonProps {
  trackingName?: string;
}

const CustomButton = forwardRef(
  (
    { trackingName, children, onClick: originalOnClick, ...rest }: CustomButtonProps,
    ref: ForwardedRef<HTMLButtonElement>
  ) => {
    const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
      // 1. 先执行埋点逻辑
      analyticsTrack('Button Clicked', {
        props: rest,
        ariaLabel: rest['aria-label'],
        trackingName,
        buttonText: typeof children === 'string' ? children : '',
      });

      // 2. 调用原组件的onClick事件,传递完整的事件对象
      if (originalOnClick) {
        originalOnClick(event);
      }
    };

    return (
      <Button
        {...rest}
        onClick={handleClick}
        ref={mergeRefs(ref)}
      >
        {children}
      </Button>
    );
  }
);

export default CustomButton;

关键改进点说明

  • 移除不必要的事件阻断:原代码中的stopPropagation和preventDefault会破坏原组件的事件冒泡和默认行为(比如表单提交、按钮默认点击逻辑),完全不需要,React事件会正常冒泡,我们只需要在自定义的handleClick中先执行埋点再调用原事件即可
  • 正确处理事件参数:直接将React的SyntheticEvent传递给原onClick,不需要用dispatchEvent(dispatchEvent需要原生DOM事件,而React SyntheticEvent是封装后的对象,无法直接用于原生事件派发)
  • 类型明确化:指定ForwardedRef的类型为HTMLButtonElement,符合MUI Button的Ref类型,消除TS报错
  • 简化Ref处理:不需要额外创建内部buttonRef,直接合并外部传入的Ref即可(如果需要获取内部按钮实例,再添加内部Ref并通过mergeRefs合并)
  • 避免lodash依赖:直接解构出originalOnClick,不需要用_.omitBy移除onClick,代码更简洁

内容的提问来源于stack exchange,提问作者Caleb Jay

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.05 07:40:30