Polimorfizem
Kako uporabiti prop `as` za spremembo renderanega HTML elementa ob ohranjanju funkcionalnosti komponente.
Prop as je temeljni vzorec v sodobnih React knjižnicah komponent, ki vam omogoča, da spremenite osnovni HTML element ali komponento, ki se rendera.
Zaslovel je v knjižnicah, kot so Styled Components, Emotion, in Chakra UI; ta vzorec nudi fleksibilnost pri izbiri semantičnega HTML-a ob ohranjanju stilov in vedenja komponente.
Prop as omogoča polimorfne komponente — komponente, ki se lahko renderajo kot različne vrste elementov ob ohranjanju svoje osnovne funkcionalnosti:
<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>Razumevanje as
Prop as vam omogoča, da presežete privzeti tip elementa komponente. Namesto da bi bila komponenta vezana na določen HTML element, jo lahko prilagodite tako, da rendera katerikoli veljavni HTML tag ali celo drugo React komponento.
Na primer:
// 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>To rendera različne HTML elemente:
<!-- Default -->
<div>Content</div>
<!-- With as="section" -->
<section>Content</section>
<!-- With as="nav" -->
<nav>Content</nav>Metode implementacije
Obstajata dva glavna pristopa za implementacijo polimorfnih komponent: ročna implementacija in uporaba Radix UI komponente Slot.
Ročna implementacija
Implementacija as prop-a uporablja dinamično renderanje komponent:
// 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>;
}Komponenta:
- Sprejema prop
ass privzetim tipom elementa - Uporabi podan element ali pade nazaj na privzetega
- Razširi vse ostale prop-e na renderani element
- Ohranja tipno varnost z TypeScript generiki
Uporaba Radix UI Slot
Radix UI nudi komponento Slot, ki predstavlja močnejšo alternativo vzorcu as. Namesto samo spreminjanja tipa elementa Slot združi prop-e s podkomponento, omogočajoč kompozicijske vzorce.
Najprej namestite paket:
npm install @radix-ui/react-slotVzorec asChild uporablja boolean prop namesto specifičnega tipa elementa:
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}
/>
)
}Zdaj ga lahko uporabite na dva načina:
// 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>Komponenta Slot:
- Klonira otroški element
- Združi prop-e komponente (className, data atribute itd.) s prop-i otroka
- Pravilno posreduje ref-e
- Ravnanje z združevanjem event handlerjev
Primerjava: as vs asChild
as prop (ročna implementacija):
// 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 z 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 mergingGlavne razlike:
| Značilnost | as prop | asChild + Slot |
|---|---|---|
| Stil API | <Button as="a"> | <Button asChild><a /></Button> |
| Tip elementa | Določen v propu | Izvlečen iz otroka |
| Sestava komponent | Omejena | Popolna podpora |
| Združevanje propov | Osnovno razširjanje | Inteligentno združevanje |
| Posredovanje referenc | Potrebna ročna nastavitev | Vgrajeno |
| Obdelovalci dogodkov | Lahko pride do konfliktov | Pravilno sestavljeno |
| Velikost knjižnice | Brez odvisnosti | Zahteva @radix-ui/react-slot |
Kdaj uporabiti kateri pristop
Uporabite prop as, kadar:
- želite enostavnejši API
- predvsem preklapljate med HTML elementi
- želite se izogniti dodatnim odvisnostim
- je komponenta preprosta in ne potrebuje kompleksnega združevanja prop-ov
Uporabite asChild + Slot, kadar:
- potrebujete kompozicijo z drugimi komponentami
- želite samodejno združevanje prop-ov
- gradite knjižnico komponent, podobno Radix UI ali shadcn/ui
- potrebujete zanesljivo posredovanje ref-ov med različnimi tipi komponent
Ključne prednosti
1. Fleksibilnost semantičnega HTML-a
Prop as zagotavlja, da lahko vedno uporabite najbolj semantično primeren HTML element:
// 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. Ponovna uporabnost komponent
Ena komponenta lahko služi več namenom, brez ustvarjanja variacij:
// 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. Izboljšave dostopnosti
Izberite elemente, ki nudijo najboljšo dostopnost za vsak kontekst:
// 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. Integracija sistema stilov
Ohranite dosledne stile pri menjavi elementov:
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>Pogosti primeri uporabe
Tipografske komponente
Ustvarite fleksibilne tekstovne komponente:
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>Postavitvene komponente
Gradite semantične layoute:
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>Interaktivni elementi
Obravnavajte različne vrste interakcij:
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>Najboljše prakse za TypeScript
Generični tipi komponent
Ustvarite popolnoma tipno varne polimorfne komponente:
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} />;
}Samodejno določanje prop-ov
Samodejno sklepajte prop-e na podlagi elementa:
// 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 errorDiskriminacijske unije
Uporabite diskriminacijske unije za prop-e specifične za element:
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} />;
}Najboljše prakse
1. Privzeto uporabite semantične elemente
Izberite smiselne privzete vrednosti, ki predstavljajo najpogostejšo rabo:
// ✅ 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. Dokumentirajte podprte elemente
Jasno navedite, kateri elementi so podprti:
interface BoxProps {
/**
* The HTML element to render as
* @default 'div'
* @example 'section', 'article', 'aside', 'main'
*/
as?: 'div' | 'section' | 'article' | 'aside' | 'main' | 'header' | 'footer';
}3. Preverite primernost elementa
Opozorite, ko se uporabijo neprimerni elementi:
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. Pravilno ravnajte z event handlerji
Zagotovite, da event handlerji delujejo čez različne elemente:
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}
/>
);
}Pogoste pasti
Neveljavna gnezditev HTML
Bodite pozorni na pravila gnezdjenja HTML-a:
// ❌ 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>Manjkajoči atributi dostopnosti
Ne pozabite dodati ustreznih ARIA atributov:
// ❌ Missing accessibility
<Box as="nav">
<MenuItems />
</Box>
// ✅ Proper accessibility
<Box as="nav" aria-label="Main navigation">
<MenuItems />
</Box>Izguba tipne varnosti
Izogibajte se preveč dovoljšnim tipom:
// ❌ 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} />;
}Premisleki glede zmogljivosti
Bodite pozorni na učinke rerenderjev:
// ❌ 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} />;
}