How to correctly wrap a React component so as to capture DOM events such as click, submit, etc, while forwarding children and refs, in Typescript?

huangapple go评论79阅读模式
英文:

How to correctly wrap a React component so as to capture DOM events such as click, submit, etc, while forwarding children and refs, in Typescript?

问题

我想要将分析跟踪事件添加到我的应用程序中的所有按钮。所有按钮都是 mui buttons,但我觉得如果我只是使用原生 HTML button,我的问题也会相同。

我的想法是创建一个"包装器"React组件,用它替换所有的 Button 组件,基本上包装了 Button 组件,添加了一些进一步的逻辑到它的事件监听器,然后继续传播附加的任何监听器。

我已经到了一个不确定如何最好做到这一点的地步,同时还要转发 ref 的地步。问题可能是"如何同时转发 ref 并使用本地 createRefref,不过我不确定。

该组件如下所示:

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;

我对类型也感到很困惑,不太清楚为什么 ForwardRefRefRefObject 的类型会有区别。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;
    }
  };
};

我已经阅读了React文档中的forwardRefs页面,这让我相信这通常是为了将跟踪事件处理添加到应用程序中的所有按钮组件的正确路径。我还阅读了各种关于停止并重新发出原生事件(例如,应用程序的各个部分使用 Formik 来捕获的 submit 事件,因此仅仅处理点击事件是不足够的)的Stack Overflow答案。这些答案帮助我达到了现在的状态:

不过,目前我收到了一个错误,关于无法在 event 对象上调用 dispatchEvent,因为显然 SyntheticEvent 类型不能传递给 dispatchEvent,我担心我可能在这条路径上走错了路。

简而言之,在不可见地包装一个组件、操作其函数和事件处理程序,然后继续调用所有这些函数和事件处理程序而不中断它们的正确方式是什么,或者也许是React推荐的方式? 在这种情况下,特定的用例是实现一个跟踪库。

我在React文档中搜索过,也在Google上搜索过一些不同的教程,查看了各种跟踪库和React集成(例如Segment和Google Analytics)的代码库,但未能找到类似的示例,这让我担心我可能走错了路,对基本概念有误解。

英文:

I would like to add analytics tracking events to all buttons in my app. All the buttons are mui buttons, but I feel my issue would be the same if I was just using native HTML buttons.

My idea is to create a "wrapper" react component that I replace all Button components with, which basically wraps the Button component, adds some further logic to its event listeners, and then continues to propagate whatever listeners are attached.

I've reached a point where I'm not sure how best to do this, while also forwarding refs. The question may be "how do I both forward refs AND use local createRef refs, though I'm not sure.

The component looks like this:

CustomButton.tsx

import _ from &#39;lodash&#39;;
import { Button, ButtonProps } from &#39;@mui/material&#39;;
import { analyticsTrack, mergeRefs } from &#39;utils/functions&#39;;
import { forwardRef, ForwardedRef, createRef } from &#39;react&#39;;
interface CustomButtonProps extends ButtonProps {
trackingName?: string;
}
const CustomButton = forwardRef(
(
{ trackingName, children, ...rest }: CustomButtonProps,
ref: ForwardedRef&lt;any&gt;
) =&gt; {
const buttonRef = createRef();
function handleClick(...args: any) {
analyticsTrack(&#39;Button Clicked&#39;, {
props: rest,
ariaLabel: rest[&#39;aria-label&#39;],
trackingName,
buttonText: typeof children === &#39;string&#39; ? children : &#39;&#39;,
});
if (rest.onClick) {
rest.onClick(args);
}
}
return (
// @ts-ignore
// eslint-disable-next-line react/jsx-no-bind
&lt;Button
{..._.omitBy(rest, [&#39;onClick&#39;])}
onMouseDown={(event) =&gt; event.stopPropagation()}
onTouchStart={(event) =&gt; event.stopPropagation()}
onClick={(event) =&gt; {
event.stopPropagation();
event.preventDefault();
handleClick();
// @ts-ignore
buttonRef.current &amp;&amp; buttonRef.current.dispatchEvent(event);
}}
// @ts-ignore
ref={mergeRefs(ref, buttonRef)}
&gt;
{children}
&lt;/Button&gt;
);
}
);
export default CustomButton;

I'm quite confused about the types as well, not quite sure why there's a difference between the type of ForwardRef, Ref., and RefObject. The mergeRefs function looks as follows:

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

I've read through the forwardRefs page on the react docs, which led me to believe that this is the path I should go down in general for adding tracking event handling to all my button components in the app. I've also been reading various stack overflow answers on stopping, and then re-issuing, native events such as submit (which various parts of the app use to be captured by Formik, for example, hence why simple click handling alone isn't sufficient). These three answers were helpful in getting me to where I am now:

https://stackoverflow.com/questions/69119297/how-can-i-call-react-useref-conditionally-in-a-portal-wrapper-and-have-better-co

https://stackoverflow.com/questions/33796267/how-to-use-refs-in-react-with-typescript

https://stackoverflow.com/questions/52705522/higher-order-react-component-for-click-tracking

At this point though, I'm getting an error about not being able to invoke dispatchEvent on the event object, as apparently SyntheticEvent types can't be passed to dispatchEvent, and I'm concerned I'm down the wrong fork on this path here.

In short, what is the correct, or perhaps react-recommended, way for "invisibly" wrapping a component, operating on its functions and event handlers, and then continuing invocations on all said functions and event handlers without stopping them? In this case with the specific usecase of implementing a tracking library.

I searched around the react docs, as well as googled for some various tutorials and looking through codebases of various tracking libraries and react integrations (such as Segment and Google Analytics) but failed to find examples along these lines, which makes me worried that I'm on an untrodden and thus incorrect path and having a fundamental misunderstanding.

答案1

得分: 0

不需要为您的用例处理内部的 ref,只需将其转发即可。

只需确保用自己的实现包装 onClick 事件处理程序,首先调用您的 analyticsTrack 函数:

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

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

演示:https://codesandbox.io/s/nameless-frog-jtdves?file=/src/App.tsx

英文:

There should be no need to handle an internal ref for your use case, just forwarding it.

Simply make sure to wrap the onClick event handler with your own implementation, to call your analyticsTrack function first:

const CustomButton = forwardRef&lt;any, CustomButtonProps&gt;(
  ({ trackingName, children, onClick, ...rest }, ref) =&gt; {
    function handleClick(event: any) {
      analyticsTrack(&quot;Button Clicked&quot;, {
        props: rest,
        ariaLabel: rest[&quot;aria-label&quot;],
        trackingName,
        buttonText: typeof children === &quot;string&quot; ? children : &quot;&quot;
      });
      if (onClick) {
        onClick(event);
      }
    }

    return (
      &lt;Button {...rest} onClick={handleClick} ref={ref}&gt;
        {children}
      &lt;/Button&gt;
    );
  }
);

Demo: https://codesandbox.io/s/nameless-frog-jtdves?file=/src/App.tsx

huangapple
  • 本文由 发表于 2023年1月9日 16:10:34
  • 转载请务必保留本文链接:https://go.coder-hub.com/75054568.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定