Búsqueda de sitios web

reactjs - ¿Cómo reutilizar de manera eficiente la interfaz AllHTMLAttributes en múltiples componentes en React 19 con TypeScript?


He estado usando React 19, y con los cambios en cómo se maneja ref, noté que necesito ajustar mi código para los componentes que heredan atributos estándar de elementos HTML.

Anteriormente, usaba forwardRef de esta manera, y funcionaba perfectamente, pasando todos los atributos de un <div> al componente.

Usando forwardRef

export const Section = forwardRef<
  HTMLDivElement,
  AllHTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
  <div {...props} className={clsx("", className)} ref={ref} />
));

Section.displayName = "Section";

Usando Ref

export const Section = ({
  className,
  ref,
  ...props
}: {
  className?: string;
  children: ReactNode;
  ref?: Ref<HTMLDivElement>;
}) => <div className={clsx("section", className)} ref={ref} {...props} />;

Ahora, para lograr el mismo comportamiento, tengo que escribir algo más verboso, creando una interface para cada componente:

interface SectionProps extends AllHTMLAttributes<HTMLDivElement> {
  className?: string;
  children?: ReactNode;
  ref?: Ref<HTMLDivElement>;
}

Esto funciona, pero hace que el código sea más repetitivo. Mi pregunta es: si tengo múltiples componentes como Card, CardHeader, CardTitle, CardContent, etc., ¿necesito crear una interfaz para cada uno de ellos?

¿Alguna idea de cómo solucionar esto?

Estoy buscando una manera de simplificar este proceso mientras mantengo la seguridad de tipos de TypeScript sin tener que definir una interfaz separada para cada componente.

Lo que he intentado:

  • Revisé la documentación más reciente de React 19.
  • Consideré crear una interfaz base para reutilizar, pero no estoy seguro de si esa es la mejor práctica.

Respuesta #1

interface SectionProps extends AllHTMLAttributes<HTMLDivElement> {
  className?: string;
  children?: ReactNode;
  ref?: Ref<HTMLDivElement>;
}

Esto funciona, pero hace que el código sea más repetitivo. Mi pregunta es: si tengo múltiples componentes como Card, CardHeader, CardTitle, CardContent, etc., ¿necesito crear una interfaz para cada uno de ellos?

...

Estoy buscando una manera de simplificar este proceso mientras mantengo la seguridad de tipos de TypeScript sin tener que definir una interfaz separada para cada componente.

En términos generales, es muy probable que quieras interfaces separadas para todos estos componentes, pero puedes abstraer las propiedades comunes que comparten en una interfaz que todos puedan extender antes de agregar sus propiedades específicas.

children, classname y ref son todas props comunes de React, podrías crear una interfaz que incluya estas por defecto usando el tipo PropsWithChildren de React:

import type {
  AllHTMLAttributes,
  Ref,
  PropsWithChildren,
} from "react";

interface AllHTMLAttributesDivProps
  extends PropsWithChildren<AllHTMLAttributes<HTMLDivElement>> {
  ref?: Ref<HTMLDivElement>;
}
interface SectionProps extends AllHTMLAttributesDivProps {
  // add section component specific props here
}

const Section = ({ className, ...props }: SectionProps) => (
  <div className={clsx("section", className)} {...props} />
);

Section.displayName = "Section";

Si no querías «bloquearte» a usar únicamente todos los atributos HTML en un elemento div, puedes hacer que tu interfaz común sea genérica en su lugar:

interface AllHTMLAttributesProps<T = HTMLDivElement>
  extends PropsWithChildren<AllHTMLAttributes<T>> {
  ref?: Ref<T>;
}

Usando una implementación del elemento div (el tipo predeterminado/de reserva):

interface SectionProps extends AllHTMLAttributesProps {
  // add section component specific props here
}

const Section = ({ className, ...props }: SectionProps) => (
  <div className={clsx("section", className)} {...props} />
);

Section.displayName = "Section";

Usando una implementación del elemento span:

interface SectionProps extends AllHTMLAttributesProps<HTMLSpanElement> {
  // add section component specific props here
}

const Section = ({ className, ...props }: SectionProps) => (
  <span className={clsx("section", className)} {...props} />
);

Section.displayName = "Section";

Respuesta #2

Mientras exploraba cómo manejar los atributos HTML y ref correctamente en React 19, encontré una solución eficiente a mi pregunta.

El Problema

El desafío consistía en permitir que un componente recibiera atributos estándar de HTML como className e id mientras se manejaba correctamente la referencia siguiendo los cambios introducidos en React 19.

La Solución

Creé una interfaz base que simplifica la reutilización y asegura que todos los atributos estén correctamente asignados. Así es como funciona:

Definiendo una Interfaz Base

import {AllHTMLAttributes, ReactNode, Ref} from "react"

interface BaseProps<T extends HTMLElement> extends AllHTMLAttributes<T> {
className: string;
children: ReactNode;
ref: Ref<T>
};

Esta interfaz:

  • Extiende AllHTMLAttributes<T>: Hereda todos los atributos HTML estándar para el tipo especificado.
  • Define Props Personalizados: Incluye propiedades como className, children y ref, según sea necesario.

Aplicando la interfaz en un componente

Con la interfaz definida, solo necesitas especificar el tipo de elemento HTML en el componente para un comportamiento correcto:

import clsx from "clsx"

export const SectionTitle = ({className, ref, ...props}: BaseProps<HTMLHeadingElement>) => (
    <h2 className={clsx("section-title", className)} ref={ref} {...props}/>
);

Por Qué Funciona Genial

  1. Reutilizable: La interfaz se puede aplicar a muchos componentes, reduciendo el código repetitivo.
  2. Tipo Seguro: Todos los atributos HTML estándar están disponibles y correctamente tipados.
  3. Flexible: Este enfoque es fácil de adaptar a otros componentes más complejos.

Reflexiones finales

Esta solución me ha funcionado bien, y espero que ayude a otros desarrolladores con desafíos similares.

Artículos relacionados