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:

  1. Sprejema prop as s privzetim tipom elementa
  2. Uporabi podan element ali pade nazaj na privzetega
  3. Razširi vse ostale prop-e na renderani element
  4. 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-slot

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

  1. Klonira otroški element
  2. Združi prop-e komponente (className, data atribute itd.) s prop-i otroka
  3. Pravilno posreduje ref-e
  4. 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 types

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

Glavne razlike:

Značilnostas propasChild + Slot
Stil API<Button as="a"><Button asChild><a /></Button>
Tip elementaDoločen v propuIzvlečen iz otroka
Sestava komponentOmejenaPopolna podpora
Združevanje propovOsnovno razširjanjeInteligentno združevanje
Posredovanje referencPotrebna ročna nastavitevVgrajeno
Obdelovalci dogodkovLahko pride do konfliktovPravilno sestavljeno
Velikost knjižniceBrez odvisnostiZahteva @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 error

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