Búsqueda de sitios web

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 ValidElement para forzar que el tipo de as sea un tipo válido, en este caso, cualquiera de:

    • Un valor de HTMLElementTagNameMap
    • Un componente general
  • (opcional) Declarar sobrecargas de función para aceptar o no el parámetro as mientras se mantienen las propiedades predeterminadas
  • Declara el cuerpo de la función que renderiza el elemento html correspondiente con sus propiedades

Por 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"
/>

Artículos relacionados