reactjs - ¿Cómo puedo crear un componente polimórfico de React con Typescript?
Me gustaría crear un botón polimórfico que en realidad pudiera ser un botón, un enlace o un enlace de enrutador.
Por ejemplo:
<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>
He leído muchos artículos, como este, pero encuentro las soluciones demasiado complicadas, especialmente cuando se trata de soportar forwardRef.
Estoy buscando algo simple de usar y fácil de entender.
Editar: Esto es para una biblioteca de componentes, así que quiero evitar cualquier dependencia de <Link> (proporcionado por react-router o librerías similares). Además, debería ser capaz de soportar otros componentes, como headless-ui <Popover.Button>
Tenía en mente una solución como la siguiente, pero todos los manejadores de eventos están tipados contra HTMLButtonElement, lo cual es obviamente incorrecto.
/* 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
Respuesta #1
Esto es lo que se me ocurrió:
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} />
}
¿Qué estoy haciendo aquí?
Declara un tipo
ValidElementpara forzar que el tipo deassea un tipo válido, en este caso, cualquiera de:- Un valor de
HTMLElementTagNameMap
- Un valor de
- Un componente general
as mientras se mantienen las propiedades predeterminadasPor supuesto, se utilizan TypeScript y TSLint y solo se ven las propiedades propias del elemento.
Uso :
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>
)
}
Respuesta #2
const Button = (props) => {
const { title, onClick, href, to } = props;
if (href) {
return (
<a href={href}>{title}</a>
)
}
return (
<Link to={to} onClick={onClick} >{title}</Link>
)
}
entonces podemos llamarlo así para el botón
<Button
title="Click me"
onClick={()=>{alert("this is button click")})
/>
para ancla
<Button
title="this is Anchor"
href="https://www.google.com"
/>
para Link
<Button
title="this is Link"
to-"/path/subpath"
/>
