Creación de componentes polimórficos con React y TypeScript

Jan 05 2023
Agregar polimorfismo a sus componentes React los hará más versátiles ya que los consumidores podrán definir el elemento representado en el DOM. Hay pasos importantes a seguir al construir estos componentes, ¡así que comencemos! ¿Por qué necesitamos componentes polimórficos? Es posible que haya notado que en algunas bibliotecas como (Material UI o Chakra UI) algunos componentes tienen la opción de permitir una propiedad as o elementType.

Agregar polimorfismo a sus componentes de React los hará más versátiles, ya que los consumidores podrán definir el elemento representado en el archivo DOM.

Hay pasos importantes a seguir al construir estos componentes, ¡así que comencemos!

¿Por qué necesitamos componentes polimórficos?

Es posible que haya notado que en algunas bibliotecas como ( Material UI o Chakra UI ) algunos componentes tienen la opción de permitir aso elementTypeprop. Esto es importante para la personalización, pero también para mejorar la accesibilidad al permitirnos usar elementos semánticos HTML al representar nuestros componentes.

Agregar un asaccesorio a un componente de React

Veamos cómo podríamos implementar algo similar a estas bibliotecas: comenzaremos con un Textcomponente React simple.

const Text = ({ as, children, ...rest }) => {
  const Component = as || "span";

  return <Component {...rest}>{children}</Component>;
};

export default function App() {
  return <Text>Text goes here</Text>; // Will render a span by default
}

<Text as="main">Text goes here</Text>;
<Text as="p">Text goes here</Text>;
<Text as="section">Text goes here</Text>;
<Text as="a" href="www.example.com">Text goes here</Text>

¡Funciona de maravilla! Ah, pero espera...

<Text as="p" href="www.example.com">
    Text goes here
</Text>

      
                

Aquí viene TypeScript

Aquí es donde entra nuestro mejor amigo TypeScript.

Necesitamos asegurarnos de que las propiedades de los Textcomponentes estén fuertemente tipadas para que puedan verificarse estáticamente durante el proceso de desarrollo.

El plan es permitir que nuestra aspropiedad acepte cualquier elemento HTML válido y luego, de alguna manera, determinar qué atributos HTML son válidos en función de ese mismo elemento (por ejemplo, una aetiqueta debe permitir el hrefatributo).

Creando nuestros tipos

Comencemos con la configuración de un tipo para la aspropiedad.

import { ElementType } from "react";

type AsProp<T extends ElementType> = {
  /**
   * The HTML element used for the root node.
   */
  as?: T;
};

Nuestro tipo Testá restringido a tipos que se extienden ElementTypedesde React, es decir, cualquier elemento HTML válido.

import { ComponentPropsWithoutRef } from "react";

ComponentPropsWithoutRef<T>

Omit<ComponentPropsWithoutRef<T>, keyof Props<T>>

Props<T> & Omit<ComponentPropsWithoutRef<T>, keyof Props<T>>

Nuestros tipos combinados deberían verse así:

type TextProps<T extends ElementType> = PropsWithChildren<AsProp<T>> &
  Omit<ComponentPropsWithoutRef<T>, keyof AsProp<T>>;

Finalmente TextPropstiene todo lo que necesitamos para Text! Juntemos todo y agreguemos el tipo a nuestro Textcomponente:

import {
  ElementType,
  ComponentPropsWithoutRef,
  PropsWithChildren
} from "react";

type AsProp<T extends ElementType> = {
  /**
   * The HTML element used for the root node.
   */
  as?: T;
};

type TextProps<T extends ElementType> = PropsWithChildren<AsProp<T>> &
  Omit<ComponentPropsWithoutRef<T>, keyof AsProp<T>>;

const Text = <T extends ElementType = "span">({
  as,
  children,
  ...rest
}: TextProps<T>) => {
  const Component = as || "span";

  return <Component {...rest}>{children}</Component>;
};

Lo mejor de nuestra configuración es que ahora ...resttambién está fuertemente tipado.

Así que esto debería funcionar:

<Text as="a" href="www.google.com">
   Text goes here
</Text>

<Text as="main" href="www.google.com">
   Text goes here
</Text>