Polimorfismo
Cómo usar la propiedad `as` para cambiar el elemento HTML renderizado mientras se preserva la funcionalidad del componente.
La propiedad as es un patrón fundamental en las bibliotecas modernas de componentes de React que te permite cambiar el elemento HTML subyacente o el componente que se renderiza.
Popularizado por bibliotecas como Styled Components, Emotion, y Chakra UI, este patrón proporciona flexibilidad para elegir HTML semántico mientras se mantienen el estilo y el comportamiento del componente.
La propiedad as permite componentes polimórficos: componentes que pueden renderizarse como distintos tipos de elemento mientras preservan su funcionalidad principal:
<Button as="a" href="/home">
Go Home
</Button>
<Button as="button" type="submit">
Submit Form
</Button>
<Button as="div" role="button" tabIndex={0}>
Custom Element
</Button>Comprendiendo as
La propiedad as te permite sobrescribir el tipo de elemento por defecto de un componente. En lugar de estar limitado a un elemento HTML específico, puedes adaptar el componente para que se renderice como cualquier etiqueta HTML válida o incluso como otro componente de React.
Por ejemplo:
// Default renders as a div
<Box>Content</Box>
// Renders as a section
<Box as="section">Content</Box>
// Renders as a nav
<Box as="nav">Content</Box>Esto renderiza diferentes elementos HTML:
<!-- Default -->
<div>Content</div>
<!-- With as="section" -->
<section>Content</section>
<!-- With as="nav" -->
<nav>Content</nav>Métodos de implementación
Hay dos enfoques principales para implementar componentes polimórficos: una implementación manual y el uso del componente Slot de Radix UI.
Implementación manual
La implementación de la propiedad as usa renderizado dinámico de componentes:
// Simplified implementation
function Component({
as: Element = 'div',
children,
...props
}) {
return <Element {...props}>{children}</Element>;
}
// More complete implementation with TypeScript
type PolymorphicProps<E extends React.ElementType> = {
as?: E;
children?: React.ReactNode;
} & React.ComponentPropsWithoutRef<E>;
function Component<E extends React.ElementType = 'div'>({
as,
children,
...props
}: PolymorphicProps<E>) {
const Element = as || 'div';
return <Element {...props}>{children}</Element>;
}El componente:
- Acepta una propiedad
ascon un tipo de elemento por defecto - Usa el elemento proporcionado o hace fallback al predeterminado
- Expande todas las demás props al elemento renderizado
- Mantiene la seguridad de tipos con genéricos de TypeScript
Uso de Radix UI Slot
Radix UI proporciona un componente Slot que ofrece una alternativa más potente al patrón de la propiedad as. En lugar de solo cambiar el tipo de elemento, Slot fusiona las props con el componente hijo, habilitando patrones de composición.
Primero, instala el paquete:
npm install @radix-ui/react-slotEl patrón asChild usa una propiedad booleana en lugar de especificar el tipo de elemento:
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
const itemVariants = cva(
"rounded-lg border p-4",
{
variants: {
variant: {
default: "bg-white",
primary: "bg-blue-500 text-white",
},
size: {
default: "h-10 px-4",
sm: "h-8 px-3",
lg: "h-12 px-6",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
function Item({
className,
variant = "default",
size = "default",
asChild = false,
...props
}: React.ComponentProps<"div"> &
VariantProps<typeof itemVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "div"
return (
<Comp
data-slot="item"
data-variant={variant}
data-size={size}
className={cn(itemVariants({ variant, size, className }))}
{...props}
/>
)
}Ahora puedes usarlo de dos maneras:
// Default: renders as a div
<Item variant="primary">Content</Item>
// With asChild: merges props with child component
<Item variant="primary" asChild>
<a href="/home">Link with Item styles</a>
</Item>El componente Slot:
- Clona el elemento hijo
- Fusiona las props del componente (className, atributos data, etc.) con las props del hijo
- Reenvía refs correctamente
- Maneja la composición de event handlers
Comparación: as vs asChild
Propiedad as (implementación manual):
// Explicit element type
<Button as="a" href="/home">Link Button</Button>
<Button as="button" type="submit">Submit Button</Button>
// Simple, predictable API
// Limited to element typesasChild con Slot:
// Implicit from child
<Button asChild>
<a href="/home">Link Button</a>
</Button>
<Button asChild>
<button type="submit">Submit Button</button>
</Button>
// More flexible composition
// Works with any component
// Better prop mergingDiferencias clave:
| Característica | as prop | asChild + Slot |
|---|---|---|
| Estilo de API | <Button as="a"> | <Button asChild><a /></Button> |
| Tipo de elemento | Especificado en la prop | Inferido del hijo |
| Composición de componentes | Limitada | Soporte completo |
| Fusión de props | Propagación básica | Fusión inteligente |
| Reenvío de refs | Requiere configuración manual | Integrado |
| Manejadores de eventos | Pueden entrar en conflicto | Composición correcta |
| Tamaño de la librería | Sin dependencia | Requiere @radix-ui/react-slot |
Cuándo usar cada enfoque
Usa la propiedad as cuando:
- Quieras una superficie de API más simple
- Principalmente cambies entre elementos HTML
- Quieras evitar dependencias adicionales
- El componente sea simple y no necesite fusión compleja de props
Usa asChild + Slot cuando:
- Necesites componer con otros componentes
- Quieras comportamiento de fusión automática de props
- Estés creando una biblioteca de componentes similar a Radix UI o shadcn/ui
- Necesites reenvío de refs confiable entre distintos tipos de componentes
Beneficios clave
1. Flexibilidad para HTML semántico
La propiedad as asegura que siempre puedas usar el elemento HTML más semánticamente apropiado:
// Navigation container
<Container as="nav" className="navigation">
<NavItems />
</Container>
// Main content area
<Container as="main" className="content">
<Article />
</Container>
// Sidebar
<Container as="aside" className="sidebar">
<Widgets />
</Container>2. Reutilización de componentes
Un componente puede servir para múltiples propósitos sin crear variantes:
// Text component used for different elements
<Text as="h1" size="2xl">Page Title</Text>
<Text as="p" size="md">Body paragraph</Text>
<Text as="span" size="sm">Inline text</Text>
<Text as="label" size="sm">Form label</Text>3. Mejoras en accesibilidad
Elige elementos que proporcionen la mejor accesibilidad para cada contexto:
// Link that looks like a button
<Button as="a" href="/signup">
Sign Up Now
</Button>
// Button that submits a form
<Button as="button" type="submit">
Submit
</Button>
// Heading with button styles
<Button as="h2" role="presentation">
Section Title
</Button>4. Integración con sistemas de estilo
Mantén estilos consistentes mientras cambias elementos:
const Card = styled.div`
padding: 1rem;
border-radius: 8px;
background: white;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
`;
// Same styles, different elements
<Card as="article">Article content</Card>
<Card as="section">Section content</Card>
<Card as="li">List item content</Card>Casos de uso comunes
Componentes de tipografía
Crea componentes de texto flexibles:
function Text({
as: Element = 'span',
variant = 'body',
...props
}) {
const className = cn(
'text-base',
variant === 'heading' && 'text-2xl font-bold',
variant === 'body' && 'text-base',
variant === 'caption' && 'text-sm text-gray-600',
props.className
);
return <Element className={className} {...props} />;
}
// Usage
<Text as="h1" variant="heading">Title</Text>
<Text as="p" variant="body">Paragraph</Text>
<Text as="figcaption" variant="caption">Caption</Text>Componentes de layout
Construye layouts semánticos:
function Flex({ as: Element = 'div', ...props }) {
return (
<Element
className={cn('flex', props.className)}
{...props}
/>
);
}
// Semantic HTML
<Flex as="header" className="justify-between">
<Logo />
<Navigation />
</Flex>
<Flex as="main" className="flex-col">
<Content />
</Flex>Elementos interactivos
Maneja distintos tipos de interacción:
function Clickable({ as: Element = 'button', ...props }) {
const isButton = Element === 'button';
const isAnchor = Element === 'a';
return (
<Element
role={!isButton && !isAnchor ? 'button' : undefined}
tabIndex={!isButton && !isAnchor ? 0 : undefined}
{...props}
/>
);
}
// Various clickable elements
<Clickable as="button" onClick={handleClick}>Button</Clickable>
<Clickable as="a" href="/link">Link</Clickable>
<Clickable as="div" onClick={handleClick}>Div Button</Clickable>Buenas prácticas en TypeScript
Tipos genéricos para componentes
Crea componentes polimórficos totalmente seguros en tipos:
type PolymorphicRef<E extends React.ElementType> =
React.ComponentPropsWithRef<E>['ref'];
type PolymorphicProps<
E extends React.ElementType,
Props = {}
> = Props &
Omit<React.ComponentPropsWithoutRef<E>, keyof Props> & {
as?: E;
};
// Component with full type safety
function Component<E extends React.ElementType = 'div'>({
as,
...props
}: PolymorphicProps<E, { customProp?: string }>) {
const Element = as || 'div';
return <Element {...props} />;
}Inferencia de props
Infiera automáticamente las props basadas en el elemento:
// Props are inferred from the element type
<Component as="a" href="/home">Home</Component> // ✅ href is valid
<Component as="div" href="/home">Home</Component> // ❌ TS error: href not valid on div
<Component as="button" type="submit">Submit</Component> // ✅ type is valid
<Component as="span" type="submit">Submit</Component> // ❌ TS errorUniones discriminadas
Usa uniones discriminadas para props específicas de cada elemento:
type ButtonProps =
| { as: 'button'; type?: 'submit' | 'button' | 'reset' }
| { as: 'a'; href: string; target?: string }
| { as: 'div'; role: 'button'; tabIndex: number };
function Button(props: ButtonProps & { children: React.ReactNode }) {
const Element = props.as;
return <Element {...props} />;
}Mejores prácticas
1. Predeterminar elementos semánticos
Elige valores por defecto significativos que representen el caso de uso más común:
// ✅ Good defaults
function Article({ as: Element = 'article', ...props }) { }
function Navigation({ as: Element = 'nav', ...props }) { }
function Heading({ as: Element = 'h2', ...props }) { }
// ❌ Too generic
function Component({ as: Element = 'div', ...props }) { }2. Documentar elementos válidos
Especifica claramente qué elementos son compatibles:
interface BoxProps {
/**
* The HTML element to render as
* @default 'div'
* @example 'section', 'article', 'aside', 'main'
*/
as?: 'div' | 'section' | 'article' | 'aside' | 'main' | 'header' | 'footer';
}3. Validar la idoneidad del elemento
Advertir cuando se usan elementos inapropiados:
function Button({ as: Element = 'button', ...props }) {
if (__DEV__ && Element === 'div' && !props.role) {
console.warn(
'Button: When using as="div", provide role="button" for accessibility'
);
}
return <Element {...props} />;
}4. Manejar correctamente los event handlers
Asegura que los event handlers funcionen en distintos elementos:
function Interactive({ as: Element = 'button', onClick, ...props }) {
const handleKeyDown = (e: React.KeyboardEvent) => {
if (Element !== 'button' && (e.key === 'Enter' || e.key === ' ')) {
onClick?.(e as any);
}
};
return (
<Element
onClick={onClick}
onKeyDown={Element !== 'button' ? handleKeyDown : undefined}
{...props}
/>
);
}Errores comunes
Anidamiento HTML inválido
Ten cuidado con las reglas de anidamiento HTML:
// ❌ Invalid - button inside button
<Button as="button">
<Button as="button">Nested</Button>
</Button>
// ❌ Invalid - div inside p
<Text as="p">
<Box as="div">Invalid nesting</Box>
</Text>
// ✅ Valid nesting
<Text as="div">
<Box as="div">Valid nesting</Box>
</Text>Atributos de accesibilidad faltantes
Recuerda añadir los atributos ARIA apropiados:
// ❌ Missing accessibility
<Box as="nav">
<MenuItems />
</Box>
// ✅ Proper accessibility
<Box as="nav" aria-label="Main navigation">
<MenuItems />
</Box>Pérdida de seguridad de tipos
Evita usar tipos excesivamente permisivos:
// ❌ Too permissive - no type safety
function Component({ as: Element = 'div', ...props }: any) {
return <Element {...props} />;
}
// ✅ Type safe
function Component<E extends React.ElementType = 'div'>({
as,
...props
}: PolymorphicProps<E>) {
const Element = as || 'div';
return <Element {...props} />;
}Consideraciones de rendimiento
Ten en cuenta las implicaciones de re-renderizado:
// ❌ Creates new component on every render
function Parent() {
const CustomDiv = (props) => <div {...props} />;
return <Component as={CustomDiv} />;
}
// ✅ Stable component reference
const CustomDiv = (props) => <div {...props} />;
function Parent() {
return <Component as={CustomDiv} />;
}