I would like to create a polymorphic button that could actually be a button, an anchor or a router link.
For instance:
<Button onClick={e => console.log("click", e)}>A button</Button>
<Button as="a" href="https://somewhere-external.com" rel="noopener noreferrer" target="_blank">
An anchor
</Button>
<Button as={Link} to="/some_page">
A Link
</Button>
I have read many articles, like this one, but I find the solutions overly complicated, especially when it comes to support forwardRef.
I'm looking for something simple to use & easy to understand.
Edit: This is for a component library, so I want to avoid any dependency to <Link> (provided by react-router or similar libs). Besides, I should be able to support other components, like headless-ui <Popover.Button>
I had in mind a solution like below, but the event handlers are all typed against HTMLButtonElement, which is obviously wrong.
/* Types file */
export type PolymorphicProps<
OwnProps,
As extends ElementType,
DefaultElement extends ElementType
> = OwnProps &
(
| (Omit<ComponentProps<As>, "as"> & { as?: As })
| (Omit<ComponentProps<As>, "as"> & { as: As })
| (Omit<ComponentProps<DefaultElement>, "as"> & { as?: never })
)
/* Component file */
const defaultElement = "button"
type OwnProps = {}
type Props<As extends ElementType = typeof defaultElement> = PolymorphicProps<
OwnProps,
As,
typeof defaultElement
>
const Button = <As extends ElementType = typeof defaultElement>(
{ as, children, ...attrs }: Props<As>,
ref: ForwardedRef<ComponentProps<As>>
) => {
const Component = as || defaultElement
return (
<Component ref={ref} {...attrs}>
{children}
</Component>
)
}
export default forwardRef(Button) as typeof Button
This is what i came up with:
type ValidElement<Props = any> = keyof Pick<HTMLElementTagNameMap, 'a' | 'button'> | ((props: Props) => ReactElement)
function PolyphormicButton <T extends ValidElement>({ as, ...props }: { as: T } & Omit<ComponentPropsWithoutRef<T>, 'as'>): ReactElement;
function PolyphormicButton ({ as, ...props }: { as?: undefined } & ComponentPropsWithoutRef <'button'>): ReactElement;
function PolyphormicButton <T extends ValidElement>({
as,
...props
}: { as?: T } & Omit<ComponentPropsWithoutRef<T>, "as">) {
const Component = as ?? "button"
return <Component {...props} />
}
ValidElement type to force the type of as to be a valid type, in this case either:
HTMLElementTagNameMapas parameter while keeping default propsOf course typescript and tslint are used and only element's own props are viewed.
Usage:
const Home = () => {
const href = "whatever"
return (
<PolymorphicButton>just a button</PolymorphicButton>
<PolymorphicButton as="a" href={href}>an anchor</PolymorphicButton>
<PolymorphicButton as={Link} to={href}>a Link component</PolymorphicButton>
)
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With