Reputation: 593
I'm trying to write a Polymorphic component in React that supports both host elements such as (div, button, etc) and custom React components.
but there's a constraint of needing whatever component is passed in to the as
prop that it must have an onClick
prop
here's my work in progress.
import React, { useCallback, useRef, useLayoutEffect } from 'react';
export type GaClickTrackerProps<Type extends React.ElementType> = {
as: Type;
fieldsObject: {
hitType: UniversalAnalytics.HitType; // 'event'
eventCategory: string;
eventAction: string;
eventLabel?: string | undefined;
eventValue?: number | undefined;
nonInteraction?: boolean | undefined;
};
onClick?: (...args: any[]) => any;
} & React.ComponentPropsWithoutRef<Type>;
export function GaClickTracker<Type extends React.ElementType>({
onClick,
fieldsObject,
as: Component,
...props
}: GaClickTrackerProps<Type>) {
const fieldsObjectRef = useRef(fieldsObject);
const onClickRef = useRef(onClick);
useLayoutEffect(() => {
onClickRef.current = onClick;
fieldsObjectRef.current = fieldsObject;
});
const handleClick: React.MouseEventHandler<Type> = useCallback((event) => {
onClickRef.current?.(event);
if (fieldsObjectRef.current) {
ga('send', fieldsObjectRef.current);
}
}, []);
return (
<Component onClick={handleClick} {...props} />
);
}
Upvotes: 0
Views: 954
Reputation: 141
Instead of going into the "polymorphic component" direction, for this particular issue it would be far better to have an util function like this:
import type { MouseEventHandler } from "react";
type Fields = {
eventAction: string;
eventCategory: string;
eventLabel?: string | undefined;
eventValue?: number | undefined;
hitType: UniversalAnalytics.HitType;
nonInteraction?: boolean | undefined;
};
export const sendAnalytics =
(fields?: Fields) =>
<Type>(handler?: MouseEventHandler<Type>): MouseEventHandler<Type> =>
event => {
handler?.(event); // We call the passed handler first
// And we only call `ga` if the we have fields and the default wasn't prevented
return !event.defaultPrevented && fields
? ga("submit", fields)
: undefined;
};
And then you can use it like this:
import { sendAnalytics } from "~/utils/sendAnalytics.js";
const sendIDKAnalytics = sendAnalytics({
eventAction: "IDK",
eventCategory: "IDK",
hitType: "IDK",
});
export const TrackedButton = ({ onClick, ...props }) => (
<button onClick={sendIDKAnalytics(onClick)} {...props} />
);
Being a curried function, you could even reuse the same settings in different components if you wanted. And if you want to "Reactify" it further, you could make it a hook, like useAnalytics
and go from there.
The "polymorphic" approach would make it unnecessarily complex with no real gains to DX:
import { createElement } from "react";
// This:
const Polymorphic = ({ as: Component, ...props }) => <Component {...props} />;
// Is not different from just doing this:
const Polymorphic = ({ as, ...props }) => createElement(as, props);
// And when using any of those, your code would look something like this,
// which is not ideal:
<Polymorphic as="main">
<Polymorphic as="nav">
<Polymorphic as="a" href="https://example.com">
Hello
</Polymorphic>
</Polymorphic>
</Polymorphic>;
Upvotes: 2