'as' Propを使ったPolymorphic Components
'as' propに型を付け、正しいprop typesを保ったままButtonを または としてrenderします。
「'as' Propを使ったPolymorphic Components」はCoddyKit上の無料React Academyレッスンです。 これはレッスン3/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはReact Academy学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 React Academyコースには全4レッスンが含まれています。
ポリモーフィックコンポーネントとは
ポリモーフィックコンポーネントは、as propに基づいて異なるHTML要素やコンポーネントとしてレンダリングされます。たとえば、正しいTypeScriptの型を保ったまま、Buttonを<a>または<button>としてレンダリングできます。
シンプルなas prop(TypeScriptなし)
ジェネリクスを使わない基本的なポリモーフィック実装です。型安全性は失われますが、概念を示すことができます。
function Box({ as: Tag = 'div', children, ...props }) {
return <Tag {...props}>{children}</Tag>;
}
// Usage:
<Box as="section" className="hero">...</Box>
<Box as="article">...</Box>ジェネリクスによる型付きas prop
ジェネリック型パラメーターを追加して、asがどの要素をレンダリングするかに応じた正しいpropsを推論できるようにします。
type PolymorphicProps<E extends React.ElementType> = {
as?: E;
} & Omit<React.ComponentPropsWithoutRef<E>, 'as'>;
function Box<E extends React.ElementType = 'div'>({
as,
...props
}: PolymorphicProps<E>) {
const Tag = as ?? 'div';
return <Tag {...props} />;
}ref対応
ポリモーフィックな型付けを維持したまま、forwardRefに対応させます。
type PolymorphicRef<E extends React.ElementType> = React.ComponentPropsWithRef<E>['ref'];
type PolymorphicPropsWithRef<E extends React.ElementType, P = {}> = P &
Omit<React.ComponentPropsWithRef<E>, keyof P> & { as?: E };
const Button = React.forwardRef(
<E extends React.ElementType = 'button'>(
{ as, ...props }: PolymorphicPropsWithRef<E>,
ref: PolymorphicRef<E>
) => {
const Tag = as ?? 'button';
return <Tag ref={ref} {...props} />;
}
);TypeScriptの型推論を実際に確認する
as='a'を設定すると、TypeScriptはコンポーネントがhrefやtargetなどアンカー固有のpropsを受け付ける一方、disabledのようなボタン固有のpropsは受け付けないことを認識します。
// TypeScript allows href because as='a':
<Box as="a" href="https://example.com">Link</Box>
// TypeScript errors if you pass href to a div:
<Box as="div" href="...">Error!</Box> // TS: href is not a valid div propカスタムpropsで拡張する
ポリモーフィックな基本型にコンポーネント固有のpropsを加えるには、それらを型定義に展開します。
type TextProps<E extends React.ElementType> = PolymorphicProps<E> & {
size?: 'sm' | 'md' | 'lg';
weight?: 'normal' | 'bold';
};
function Text<E extends React.ElementType = 'p'>({
as,
size = 'md',
weight = 'normal',
className,
...props
}: TextProps<E>) {
const Tag = as ?? 'p';
return (
<Tag
className={[`text-${size}`, `font-${weight}`, className].filter(Boolean).join(' ')}
{...props}
/>
);
}実際の使用例:RouterLinkとしてレンダリングするLink
toが指定されている場合はReact RouterのLinkとして、指定されていない場合は通常のボタンとしてレンダリングされるButtonです。
import { Link } from 'react-router-dom';
type ButtonProps =
| { to: string; href?: never } & React.ComponentPropsWithoutRef<typeof Link>
| { href: string; to?: never } & React.ComponentPropsWithoutRef<'a'>
| { to?: never; href?: never } & React.ComponentPropsWithoutRef<'button'>;
function Button({ to, href, ...props }: ButtonProps) {
if (to) return <Link to={to} {...props} />;
if (href) return <a href={href} {...props} />;
return <button type="button" {...props} />;
}デザインシステムのTextコンポーネント
ポリモーフィックなTextコンポーネントです。デフォルトでは<p>としてレンダリングされますが、h1〜h6、span、labelとしてもレンダリングできます。
<Text as="h1" size="xl">Page Title</Text>
<Text as="span" weight="bold">Inline bold</Text>
<Text as="label" htmlFor="email">Email</Text> // htmlFor is valid because as='label'as propに対する制約
extends React.ElementTypeを使うと、HTML要素の文字列('div'や'a')とReactコンポーネントの両方を受け付けられます。
function Card<E extends React.ElementType = 'div'>({ as, ...props }: PolymorphicProps<E>) {
const Tag = as ?? 'div';
return <Tag {...props} />;
}
// Works with HTML elements:
<Card as="section" aria-label="Products" />
// Works with React components:
<Card as={motion.div} animate={{ opacity: 1 }} />よくある落とし穴を避ける
as propを要素に展開しないでください(有効なHTML属性ではありません)。rest propsを展開する前に、必ず分割代入で取り出してください。
// Bad:
function Box({ as: Tag = 'div', ...props }) {
return <Tag as={...} {...props} />; // 'as' attribute on div is invalid HTML
}
// Good:
function Box({ as: Tag = 'div', ...props }) {
return <Tag {...props} />; // 'as' is used to pick Tag, not passed to element
}パフォーマンスに関する考慮事項
ポリモーフィックコンポーネントによるパフォーマンスのオーバーヘッドは最小限です。as propは、要素型の文字列またはコンポーネント参照を保持する単なる変数だからです。
確認問題
ポリモーフィックコンポーネントでrest propsを要素に展開する前に、as propを分割代入する必要があるのはなぜですか?
まとめ
ポリモーフィックコンポーネントは、任意の要素としてレンダリングしながら正しいTypeScriptのpropsを維持するために、ジェネリックなas?: E extends React.ElementType propを使用します。rest propsを展開する前にasを分割代入してください。型に要素固有のすべての属性を含めるには、React.ComponentPropsWithoutRefを使用します。
よくある質問
「'as' Propを使ったPolymorphic Components」レッスンは無料ですか?
はい。「'as' Propを使ったPolymorphic Components」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、React Academyコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 React Academyコースには全4レッスンが含まれています。
「'as' Propを使ったPolymorphic Components」で何を学びますか?
'as' propに型を付け、正しいprop typesを保ったままButtonを または としてrenderします。 ブラウザで直接実行するハンズオンコードでReact Academyを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。
React Academyを始めるのに経験は必要ですか?
事前経験は必要ありません。CoddyKitのReact Academyは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン3/4です。
「'as' Propを使ったPolymorphic Components」レッスンにはどのくらい時間がかかりますか?
ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。
このReact Academyレッスンでコードを書いて実行できますか?
はい。すべてのReact Academyレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。
このコースのすべてのレッスン
- Component VariantsのDiscriminated Unions
- ReactにおけるConditional TypesとMapped Types
- 'as' Propを使ったPolymorphic Components
- 型安全なFormsとAPI Response Contracts