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:
How can I call React useRef conditionally in a Portal wrapper and have better code?
How to use refs in React with Typescript
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.