Polimorfizm

Bileşen işlevselliğini korurken render edilen HTML öğesini değiştirmek için `as` prop'unun nasıl kullanılacağını.

as prop'u, render edilen temel HTML öğesini veya bileşeni değiştirmenize olanak tanıyan modern React bileşen kütüphanelerinde temel bir desendir.

Styled Components, Emotion ve Chakra UI gibi kütüphaneler tarafından popülerleştirilen bu desen, bileşen stillerini ve davranışını korurken semantik HTML seçme esnekliği sağlar.

as prop'u, çekirdek işlevselliğini korurken farklı öğe tipleri olarak render edilebilen polimorfik bileşenlere olanak tanır:

<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>

as'ı Anlamak

as prop'u, bir bileşenin varsayılan öğe tipini geçersiz kılmanıza izin verir. Belirli bir HTML öğesiyle kilitlenmek yerine, bileşeni herhangi bir geçerli HTML etiketi veya başka bir React bileşeni olarak render edecek şekilde uyarlayabilirsiniz.

Örneğin:

// 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>

Bu, farklı HTML öğelerinin render edilmesini sağlar:

<!-- Default -->
<div>Content</div>

<!-- With as="section" -->
<section>Content</section>

<!-- With as="nav" -->
<nav>Content</nav>

Uygulama Yöntemleri

Polimorfik bileşenleri uygulamanın iki ana yaklaşımı vardır: manuel bir uygulama ve Radix UI'nin Slot bileşenini kullanmak.

Manuel Uygulama

as prop'u uygulaması dinamik bileşen render'ı kullanır:

// 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>;
}

Bileşen:

  1. Varsayılan bir öğe tipi ile as prop'unu kabul eder
  2. Sağlanan öğeyi kullanır veya varsayılanı geri döner
  3. Diğer tüm prop'ları render edilen öğeye yayar
  4. TypeScript generikleri ile tip güvenliğini korur

Radix UI Slot Kullanımı

Radix UI Slot bileşenini sağlar; bu, as prop deseni için daha güçlü bir alternatiftir. Sadece öğe tipini değiştirmek yerine, Slot prop'ları çocuk bileşenle birleştirir ve kompozisyon desenlerine olanak tanır.

Önce paketi yükleyin:

npm install @radix-ui/react-slot

asChild deseni, öğe tipini belirtmek yerine boolean bir prop kullanır:

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}
    />
  )
}

Artık iki şekilde kullanabilirsiniz:

// 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>

Slot bileşeni:

  1. Çocuk öğeyi klonlar
  2. Bileşenin prop'larını (className, data atributeleri vb.) çocuğun prop'larıyla birleştirir
  3. Ref'leri doğru şekilde iletir
  4. Olay işleyici kompozisyonunu işler

Karşılaştırma: as vs asChild

as prop'u (manuel uygulama):

// 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 types

asChild ile 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 merging

Temel farklar:

Özellikas prop'uasChild + Slot
API Stili<Button as="a"><Button asChild><a /></Button>
Öğe TipiProp'ta belirtilirÇocuktan çıkarılır
Bileşen KompozisyonuSınırlıTam destek
Prop BirleştirmeTemel yaymaAkıllı birleştirme
Ref İletimiManuel ayar gerekirDahili
Olay İşleyicileriÇakışma olabilirDoğru şekilde bileştirilir
Kütüphane BoyutuBağımlılık yok@radix-ui/react-slot gerektirir

Hangi Yaklaşım Ne Zaman Kullanılmalı

as prop'unu kullanın eğer:

  • Daha basit bir API yüzeyi istiyorsanız
  • Öncelikle HTML öğeleri arasında geçiş yapıyorsanız
  • Ek bağımlılıklardan kaçınmak istiyorsanız
  • Bileşen basit ve karmaşık prop birleştirmeye ihtiyaç duymuyorsa

asChild + Slot kullanın eğer:

  • Diğer bileşenlerle kompozisyon yapmanız gerekiyorsa
  • Otomatik prop birleştirme davranışı istiyorsanız
  • Radix UI veya shadcn/ui benzeri bir bileşen kütüphanesi inşa ediyorsanız
  • Farklı bileşen tipleri arasında güvenilir ref iletimi gerekiyorsa

Temel Avantajlar

1. Semantik HTML Esnekliği

as prop'u, her zaman en semantik uygun HTML öğesini kullanabilmenizi sağlar:

// 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. Bileşen Yeniden Kullanılabilirliği

Tek bir bileşen, varyantlar oluşturmadan birden çok amaç için hizmet edebilir:

// 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. Erişilebilirlik İyileştirmeleri

Her bağlam için en iyi erişilebilirliği sağlayan öğeleri seçin:

// 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. Stil Sistemi Entegrasyonu

Öğeleri değiştirirken tutarlı stilleri koruyun:

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>

Yaygın Kullanım Senaryoları

Tipografi Bileşenleri

Esnek metin bileşenleri oluşturun:

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>

Düzen (Layout) Bileşenleri

Semantik düzenler oluşturun:

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>

Etkileşimli Öğeler

Farklı etkileşim türlerini ele alı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>

TypeScript En İyi Uygulamaları

Generik Bileşen Tipleri

Tam tip güvenli polimorfik bileşenler oluşturun:

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} />;
}

Prop'ların Çıkarımı

Öğe tipine göre prop'ları otomatik olarak çıkarın:

// 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 error

Ayırt Edilmiş Birlikler (Discriminated Unions)

Öğe-özgü prop'lar için ayırt edilmiş birlikler kullanın:

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} />;
}

En İyi Uygulamalar

1. Semantik Öğelere Varsayılan Yapın

En yaygın kullanım durumunu temsil eden anlamlı varsayılanlar seçin:

// ✅ 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. Desteklenen Öğeleri Belgeleyin

Hangi öğelerin desteklendiğini açıkça belirtin:

interface BoxProps {
  /**
   * The HTML element to render as
   * @default 'div'
   * @example 'section', 'article', 'aside', 'main'
   */
  as?: 'div' | 'section' | 'article' | 'aside' | 'main' | 'header' | 'footer';
}

3. Öğe Uygunluğunu Doğrulayın

Uygun olmayan öğeler kullanıldığında uyarı verin:

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. Olay İşleyicilerini Doğru Ele Alın

Olay işleyicilerinin farklı öğelerde çalıştığından emin olun:

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}
    />
  );
}

Yaygın Tuzaklar

Geçersiz HTML İçi İçe Geçme

HTML iç içe geçme kurallarına dikkat edin:

// ❌ 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>

Eksik Erişilebilirlik Atributları

Uygun ARIA atributelerini eklemeyi unutmayın:

// ❌ Missing accessibility
<Box as="nav">
  <MenuItems />
</Box>

// ✅ Proper accessibility
<Box as="nav" aria-label="Main navigation">
  <MenuItems />
</Box>

Tip Güvenliği Kaybı

Aşırı izin verici tiplerden kaçının:

// ❌ 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} />;
}

Performans Düşünceleri

Yine render etkilerini göz önünde bulundurun:

// ❌ 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} />;
}