Polymorphismus
Wie man die `as`-Prop verwendet, um das gerenderte HTML-Element zu ändern und gleichzeitig die Funktionalität der Komponente beizubehalten.
Die as-Prop ist ein grundlegendes Muster in modernen React-Komponentenbibliotheken, das es ermöglicht, das zugrunde liegende HTML-Element oder die Komponente zu ändern, die gerendert wird.
Popularisiert durch Bibliotheken wie Styled Components, Emotion, und Chakra UI, bietet dieses Muster die Flexibilität, semantisches HTML zu wählen und gleichzeitig Styling und Verhalten der Komponente beizubehalten.
Die as-Prop ermöglicht polymorphe Komponenten - Komponenten, die als verschiedene Elementtypen gerendert werden können, während ihre Kernfunktionalität erhalten bleibt:
<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>Verständnis von as
Die as-Prop erlaubt es Ihnen, den standardmäßigen Elementtyp einer Komponente zu überschreiben. Anstatt auf ein bestimmtes HTML-Element festgelegt zu sein, können Sie die Komponente so anpassen, dass sie als jedes gültige HTML-Tag oder sogar als eine andere React-Komponente gerendert wird.
Zum Beispiel:
// 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>Das rendert unterschiedliche HTML-Elemente:
<!-- Default -->
<div>Content</div>
<!-- With as="section" -->
<section>Content</section>
<!-- With as="nav" -->
<nav>Content</nav>Implementierungsansätze
Es gibt zwei Hauptansätze zur Implementierung polymorpher Komponenten: eine manuelle Implementierung und die Verwendung des Slot-Components von Radix UI.
Manuelle Implementierung
Die Implementierung der as-Prop verwendet dynamisches Component-Rendering:
// 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>;
}Die Komponente:
- Akzeptiert eine
as-Prop mit einem Standard-Elementtyp - Verwendet das angegebene Element oder fällt auf das Standard zurück
- Spreadet alle anderen Props auf das gerenderte Element
- Bewahrt Typensicherheit durch TypeScript-Generics
Verwendung von Radix UI Slot
Radix UI stellt ein Slot-Component zur Verfügung, das eine leistungsfähigere Alternative zum as-Prop-Muster bietet. Anstatt nur den Elementtyp zu ändern, verbindet Slot Props mit der Kindkomponente und ermöglicht damit Kompositionsmuster.
Installieren Sie zuerst das Paket:
npm install @radix-ui/react-slotDas asChild-Muster verwendet eine boolean-Prop anstelle der Angabe des Elementtyps:
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}
/>
)
}Jetzt können Sie es auf zwei Arten verwenden:
// 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>Das Slot-Component:
- Klont das Kindelement
- Verschmilzt die Props der Komponente (className, data-Attribute, etc.) mit den Props des Kindes
- Forwardet refs korrekt
- Handhabt die Komposition von Event-Handlern
Vergleich: as vs asChild
as prop (manuelle Implementierung):
// 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 with 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 mergingWesentliche Unterschiede:
| Merkmal | as prop | asChild + Slot |
|---|---|---|
| API-Stil | <Button as="a"> | <Button asChild><a /></Button> |
| Elementtyp | Im Prop angegeben | Vom Kind abgeleitet |
| Komponentenkomposition | Eingeschränkt | Volle Unterstützung |
| Props-Zusammenführung | Einfaches Spread | Intelligente Verschmelzung |
| Ref-Weiterleitung | Manuelle Einrichtung nötig | Integriert |
| Ereignishandler | Können konfligieren | Werden korrekt komponiert |
| Bibliotheksgröße | Keine Abhängigkeit | Erfordert @radix-ui/react-slot |
Wann Sie welchen Ansatz verwenden sollten
Verwenden Sie die as-Prop, wenn:
- Sie eine einfachere API wollen
- Sie hauptsächlich zwischen HTML-Elementen wechseln
- Sie zusätzliche Abhängigkeiten vermeiden möchten
- Die Komponente einfach ist und keine komplexe Props-Verschmelzung benötigt
Verwenden Sie asChild + Slot, wenn:
- Sie mit anderen Komponenten komponieren müssen
- Sie automatische Props-Verschmelzung wünschen
- Sie eine Komponentenbibliothek ähnlich Radix UI oder shadcn/ui bauen
- Sie zuverlässiges Ref-Forwarding über verschiedene Komponententypen hinweg benötigen
Hauptvorteile
1. Semantische HTML-Flexibilität
Die as-Prop stellt sicher, dass Sie immer das semantisch passendste HTML-Element verwenden können:
// 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. Wiederverwendbarkeit von Komponenten
Eine Komponente kann mehrere Zwecke erfüllen, ohne Varianten erstellen zu müssen:
// 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. Verbesserte Zugänglichkeit
Wählen Sie Elemente, die den besten Zugang für den jeweiligen Kontext bieten:
// 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. Integration ins Stylesystem
Behalten Sie konsistente Styles bei, während Sie Elemente ändern:
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>Häufige Anwendungsfälle
Typografie-Komponenten
Erstellen Sie flexible Textkomponenten:
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>Layout-Komponenten
Erstellen Sie semantische Layouts:
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>Interaktive Elemente
Behandeln Sie verschiedene Interaktionstypen:
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>TypeScript Best Practices
Generische Komponententypen
Erstellen Sie vollständig typsichere polymorphe Komponenten:
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} />;
}Props ableiten
Leiten Sie Props automatisch basierend auf dem Element ab:
// 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 errorDiskriminierte Unions
Verwenden Sie discriminated unions für element-spezifische Props:
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} />;
}Beste Vorgehensweisen
1. Standardmäßig semantische Elemente verwenden
Wählen Sie sinnvolle Defaults, die den häufigsten Anwendungsfall repräsentieren:
// ✅ 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. Gültige Elemente dokumentieren
Geben Sie klar an, welche Elemente unterstützt werden:
interface BoxProps {
/**
* The HTML element to render as
* @default 'div'
* @example 'section', 'article', 'aside', 'main'
*/
as?: 'div' | 'section' | 'article' | 'aside' | 'main' | 'header' | 'footer';
}3. Angemessenheit der Elemente validieren
Warnen Sie, wenn ungeeignete Elemente verwendet werden:
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. Ereignishandler korrekt behandeln
Stellen Sie sicher, dass Event-Handler über verschiedene Elemente hinweg funktionieren:
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}
/>
);
}Häufige Fallstricke
Ungültige HTML-Verschachtelung
Achten Sie auf HTML-Verschachtelungsregeln:
// ❌ 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>Fehlende Zugänglichkeitsattribute
Denken Sie daran, passende ARIA-Attribute hinzuzufügen:
// ❌ Missing accessibility
<Box as="nav">
<MenuItems />
</Box>
// ✅ Proper accessibility
<Box as="nav" aria-label="Main navigation">
<MenuItems />
</Box>Verlust der Typensicherheit
Vermeiden Sie zu permissive Typen:
// ❌ 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} />;
}Leistungsaspekte
Beachten Sie mögliche Re-Render-Implikationen:
// ❌ 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} />;
}