Polimorfisme
Cara menggunakan prop `as` untuk mengubah elemen HTML yang dirender sambil mempertahankan fungsionalitas komponen.
Prop as adalah pola dasar dalam pustaka komponen React modern yang memungkinkan Anda mengubah elemen HTML atau komponen dasar yang dirender.
Dipopulerkan oleh pustaka seperti Styled Components, Emotion, dan Chakra UI, pola ini memberikan fleksibilitas dalam memilih HTML semantik sambil mempertahankan styling dan perilaku komponen.
Prop as memungkinkan komponen polimorfik — komponen yang dapat dirender sebagai berbagai tipe elemen sambil mempertahankan fungsionalitas inti mereka:
<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>Memahami as
Prop as memungkinkan Anda menimpa tipe elemen default dari sebuah komponen. Alih-alih terikat pada elemen HTML tertentu, Anda dapat menyesuaikan komponen untuk dirender sebagai tag HTML yang valid atau bahkan komponen React lain.
Sebagai contoh:
// 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>Ini merender elemen HTML yang berbeda:
<!-- Default -->
<div>Content</div>
<!-- With as="section" -->
<section>Content</section>
<!-- With as="nav" -->
<nav>Content</nav>Metode Implementasi
Ada dua pendekatan utama untuk mengimplementasikan komponen polimorfik: implementasi manual dan menggunakan komponen Slot dari Radix UI.
Implementasi Manual
Implementasi prop as menggunakan rendering komponen dinamis:
// 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>;
}Komponen ini:
- Menerima prop
asdengan tipe elemen default - Menggunakan elemen yang disediakan atau fallback ke default
- Menyebarkan semua prop lain ke elemen yang dirender
- Mempertahankan keamanan tipe menggunakan generik TypeScript
Menggunakan Radix UI Slot
Radix UI menyediakan komponen Slot yang menawarkan alternatif lebih kuat untuk pola prop as. Alih-alih hanya mengubah tipe elemen, Slot menggabungkan prop dengan komponen anak, memungkinkan pola komposisi.
Pertama, pasang paket:
npm install @radix-ui/react-slotPola asChild menggunakan prop boolean alih-alih menentukan tipe elemen:
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}
/>
)
}Sekarang Anda dapat menggunakannya dalam dua cara:
// 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>Komponen Slot:
- Mengkloning elemen anak
- Menggabungkan prop-komponen (className, atribut data, dll.) dengan prop anak
- Meneruskan ref dengan benar
- Menangani komposisi event handler
Perbandingan: as vs asChild
as prop (implementasi 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 dengan 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 mergingPerbedaan utama:
| Fitur | as prop | asChild + Slot |
|---|---|---|
| Gaya API | <Button as="a"> | <Button asChild><a /></Button> |
| Tipe Elemen | Ditentukan di prop | Diinfer dari anak |
| Komposisi Komponen | Terbatas | Dukungan penuh |
| Penggabungan Prop | Penyebaran dasar | Penggabungan cerdas |
| Penerusan ref | Perlu pengaturan manual | Bawaan |
| Penanganan event | Dapat konflik | Dikomposisikan dengan benar |
| Ukuran pustaka | Tanpa dependensi | Memerlukan @radix-ui/react-slot |
Kapan Menggunakan Masing-masing Pendekatan
Gunakan prop as ketika:
- Anda menginginkan permukaan API yang lebih sederhana
- Anda terutama beralih antar elemen HTML
- Anda ingin menghindari dependensi tambahan
- Komponen sederhana dan tidak membutuhkan penggabungan prop yang kompleks
Gunakan asChild + Slot ketika:
- Anda perlu melakukan komposisi dengan komponen lain
- Anda menginginkan perilaku penggabungan prop otomatis
- Anda membangun pustaka komponen mirip Radix UI atau shadcn/ui
- Anda membutuhkan penerusan ref yang andal di berbagai tipe komponen
Manfaat Utama
1. Fleksibilitas HTML Semantik
Prop as memastikan Anda selalu dapat menggunakan elemen HTML yang paling semantis sesuai konteks:
// 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. Kegunaan Ulang Komponen
Satu komponen dapat melayani berbagai tujuan tanpa membuat varian terpisah:
// 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. Peningkatan Aksesibilitas
Pilih elemen yang memberikan aksesibilitas terbaik untuk tiap konteks:
// 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. Integrasi Sistem Styling
Pertahankan styling konsisten sambil mengganti elemen:
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>Kasus Penggunaan Umum
Komponen Tipografi
Buat komponen teks yang fleksibel:
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>Komponen Layout
Bangun layout semantik:
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>Elemen Interaktif
Tangani berbagai tipe interaksi:
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>Praktik Terbaik TypeScript
Tipe Komponen Generik
Buat komponen polimorfik yang sepenuhnya aman secara tipe:
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} />;
}Menginferensi Props
Secara otomatis menurunkan tipe props berdasarkan elemen:
// 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 errorDiscriminated Unions
Gunakan discriminated unions untuk prop spesifik-elemen:
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} />;
}Praktik Terbaik
1. Default ke Elemen Semantis
Pilih default yang bermakna dan mewakili kasus penggunaan umum:
// ✅ 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. Dokumentasikan Elemen yang Didukung
Tentukan dengan jelas elemen mana yang didukung:
interface BoxProps {
/**
* The HTML element to render as
* @default 'div'
* @example 'section', 'article', 'aside', 'main'
*/
as?: 'div' | 'section' | 'article' | 'aside' | 'main' | 'header' | 'footer';
}3. Validasi Kesesuaian Elemen
Berikan peringatan saat elemen yang tidak tepat digunakan:
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. Tangani Event Handler dengan Benar
Pastikan event handler bekerja di berbagai elemen:
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}
/>
);
}Jebakan Umum
Penempatan HTML yang Tidak Valid
Perhatikan aturan penempatan 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>Atribut Aksesibilitas yang Hilang
Ingat untuk menambahkan atribut ARIA yang sesuai:
// ❌ Missing accessibility
<Box as="nav">
<MenuItems />
</Box>
// ✅ Proper accessibility
<Box as="nav" aria-label="Main navigation">
<MenuItems />
</Box>Kehilangan Keamanan Tipe
Hindari penggunaan tipe yang terlalu permisif:
// ❌ 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} />;
}Pertimbangan Kinerja
Perhatikan implikasi re-render:
// ❌ 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} />;
}