React’te “Polymorphic Component” Deseni: Tek Bileşenle Farklı HTML Etiketleri (TypeScript ile)
Button gibi bir bileşeni hem <button> hem <a> olarak güvenli biçimde kullanmak için polymorphic desen.
React projelerinde sıkça “Button gibi görünsün ama link gibi davransın” ihtiyacı çıkar. Kimi zaman bir CTA bir sayfaya gider (link), kimi zaman form gönderir (button). İki ayrı bileşen yazmak yerine, tek bileşeni farklı HTML etiketleriyle (as prop) kullanmak hem tutarlılık hem de erişilebilirlik açısından büyük kazanç sağlar.
Bu yazıda, TypeScript ile tip güvenli polymorphic component desenini kısa ve pratik bir örnekle kuracağız.
Problem: Aynı UI, farklı semantik
Aşağıdaki iki kullanım aynı tasarım diline sahip olabilir ama semantik farklıdır:
- Sayfaya yönlendirme:
a(veya router link) - Aksiyon tetikleme:
button
Yanlış semantik (ör. her şeyi div yapmak) erişilebilirliği düşürür ve beklenmeyen davranışlara yol açar.
Hedef: Button bileşeni as ile şekil değiştirsin
İstediğimiz API:
<Button onClick={save}>Kaydet</Button>
<Button as="a" href="/pricing">Fiyatları Gör</Button>
Ve TypeScript şunu garanti etsin:
as="a"ikenhrefzorunlu/uygun olsun- normal kullanımda
hrefverilirse hata versin - ref/prop tipleri doğru taşınsın
Çözüm: Tip güvenli polymorphic altyapı
Aşağıdaki yardımcı tipler, seçilen elementin prop’larını “ödünç alıp” kendi prop’larımızla birleştirir.
import * as React from "react";
type AsProp<E extends React.ElementType> = {
as?: E;
};
type PropsToOmit<E extends React.ElementType, P> = keyof (AsProp<E> & P);
type PolymorphicComponentProps<
E extends React.ElementType,
P
> = React.PropsWithChildren<P & AsProp<E>> &
Omit<React.ComponentPropsWithoutRef<E>, PropsToOmit<E, P>>;
type PolymorphicRef<E extends React.ElementType> =
React.ComponentPropsWithRef<E>["ref"];
Button örneği
Kendi tasarım prop’larımızı ekleyelim: variant ve size.
type ButtonOwnProps = {
variant?: "primary" | "secondary";
size?: "sm" | "md";
};
type ButtonProps<E extends React.ElementType> =
PolymorphicComponentProps<E, ButtonOwnProps>;
const Button = React.forwardRef(
<E extends React.ElementType = "button">(
{ as, variant = "primary", size = "md", children, ...rest }: ButtonProps<E>,
ref: PolymorphicRef<E>
) => {
const Component = as || "button";
const className = [
"btn",
`btn--${variant}`,
`btn--${size}`,
// kullanıcı className verdiyse yakalayalım
(rest as any).className,
]
.filter(Boolean)
.join(" ");
return (
<Component ref={ref} {...rest} className={className}>
{children}
</Component>
);
}
);
Button.displayName = "Button";
Kullanım
export function Example() {
return (
<div style={{ display: "flex", gap: 12 }}>
<Button onClick={() => console.log("save")}>Kaydet</Button>
<Button as="a" href="/pricing" variant="secondary">
Fiyatları Gör
</Button>
{/* TypeScript: href, button için geçersiz -> hata */}
{/* <Button href="/oops">Yanlış</Button> */}
</div>
);
}
Neye dikkat etmeli?
- Semantik seçimi: Navigasyon =
a, aksiyon =button. Bu ayrım klavye/kullanıcı davranışlarında kritiktir. - Router entegrasyonu:
as={Link}(React Router / Next.js Link) gibi kullanımda da prop tipleri otomatik taşınır. - disabled davranışı:
aetiketindedisabledyoktur; “disabled link” gerekiyorsaaria-disabled,tabIndex={-1}ve click engelleme gibi ek kurallar gerekebilir.
Sonuç
Polymorphic component deseni, tasarım sistemi benzeri yapılarda tek bileşenle doğru HTML semantiğini koruyarak tekrar kullanım sağlar. TypeScript ile kurulduğunda, hatalı prop kullanımını daha yazarken yakalarsınız.