英文:
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
并使用本地 createRef
的 ref
,不过我不确定。
该组件如下所示:
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;
我对类型也感到很困惑,不太清楚为什么 ForwardRef
、Ref
和 RefObject
的类型会有区别。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答案。这些答案帮助我达到了现在的状态:
- 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
不过,目前我收到了一个错误,关于无法在 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 button
s.
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 ref
s. 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 '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;
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<Element | null>[]) => {
return ( node: Element ) => {
for (const ref of refs) {
// @ts-ignore
ref && !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/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<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>
);
}
);
Demo: https://codesandbox.io/s/nameless-frog-jtdves?file=/src/App.tsx
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论