コード例
ヘッダー(タグ子コンポーネントとして持つ)、本文、を要素として持つArticle
というコンポーネントを作ってみる
Article.tsx
interface ArticleComposition {
Header: React.FC<ArticleHeaderProps>;
Body: React.FC<ArticleBodyProps>;
HeaderTag: React.FC<ArticleHeaderTagProps>;
}
interface ArticleProps {
children: React.ReactNode;
}
interface ArticleHeaderProps {
title: string;
children: React.ReactNode;
}
interface ArticleHeaderTagProps {
tag: string;
}
interface ArticleBodyProps {
content: string;
}
export const Article: React.FC<ArticleProps> & ArticleComposition = ({ children }) => {
return <>{children}</>;
};
const ArticleHeader: React.FC<ArticleHeaderProps> = ({ children, title }) => {
return (
<div>
<h2>{title}</h2>
<div>{children}</div>
</div>
);
};
const ArticleHeaderTag: React.FC<ArticleHeaderTagProps> = ({ tag }) => {
return <div>{tag}</div>;
};
const ArticleBody: React.FC<ArticleBodyProps> = ({ content }) => {
return <div>{content}</div>;
};
Article.Header = ArticleHeader;
Article.HeaderTag = ArticleHeaderTag;
Article.Body = ArticleBody;
使用例
<Article>
<Article.Header title="記事タイトル">
<Article.HeaderTag tag="技術" />
</Article.Header>
<Article.Body content="記事の本文内容..." />
</Article>
解説
ArticleComposition インターフェース
interface ArticleComposition {
Header: React.FC<ArticleHeaderProps>;
Body: React.FC<ArticleBodyProps>;
HeaderTag: React.FC<ArticleHeaderTagProps>;
}
上記は、Articleコンポーネントが持つサブコンポーネントの構成を定義しています。
-
Header:
ArticleHeaderProps
をpropsとして持つコンポーネント -
Body:
ArticleBodyProps
をpropsとして持つコンポーネント
HeaderTag:ArticleHeaderTagProps
をpropsとして持つコンポーネント
interface ArticleProps {
children: React.ReactNode;
}
interface ArticleHeaderProps {
title: string;
children: React.ReactNode;
}
interface ArticleHeaderTagProps {
tag: string;
}
interface ArticleBodyProps {
content: string;
}
上記は、各コンポーネントのProps定義を行っています。
export const Article: React.FC<ArticleProps> & ArticleComposition = ({ children }) => {
return <>{children}</>;
};
const ArticleHeader: React.FC<ArticleHeaderProps> = ({ children, title }) => {
return (
<div>
<h2>{title}</h2>
<div>{children}</div>
</div>
);
};
const ArticleHeaderTag: React.FC<ArticleHeaderTagProps> = ({ tag }) => {
return <div>{tag}</div>;
};
const ArticleBody: React.FC<ArticleBodyProps> = ({ content }) => {
return <div>{content}</div>;
};
上記部分では、コンポーネントを実装しています:
- 特に、
Article
の型定義は、React.ReactNode & React.FC<ArticleHeaderTagProps>
という少し複雑な型定義です - この
&
は、インターセクション型と呼ばれ、両方の型の特性を持つ必要があることを示します - つまりは、Header、Body、HeaderTagをサブコンポーネントとして持つ、
FC<ArticleProps>
型のコンポーネントということを示します
Article.Header = ArticleHeader;
Article.HeaderTag = ArticleHeaderTag;
Article.Body = ArticleBody;
上記の部分では、サブコンポーネントをメインのArticleコンポーネントに割り当てています
どういう時に有効と感じるか
私は、孫コンポーネント以降の情報が適度に可視化されることに特に利点があると感じます。
例として、Compound Patternを使わずに、単にコンポーネント分割させた場合を見てみます。
使用例
const MyArticle = () => (
<Article
title="Reactの最新機能"
tag="フロントエンド"
content="Reactの最新バージョンでは..."
/>
);
Articleの各コンポーネントの詳細はコチラ
ArticleHeaderコンポーネントの定義
interface ArticleHeaderProps {
title: string;
tag: string;
}
const ArticleHeader: React.FC<ArticleHeaderProps> = ({ title, tag }) => (
<div className="article-header">
<h2>{title}</h2>
<ArticleTag tag={tag} />
</div>
);
ArticleTagコンポーネントの定義
interface ArticleTagProps {
tag: string;
}
const ArticleTag: React.FC<ArticleTagProps> = ({ tag }) => (
<div className="article-tag">{tag}</div>
);
ArticleBodyコンポーネントの定義
interface ArticleBodyProps {
content: string;
}
const ArticleBody: React.FC<ArticleBodyProps> = ({ content }) => (
<div className="article-body">{content}</div>
);
Articleコンポーネントの定義
interface ArticleProps {
title: string;
tag: string;
content: string;
}
const Article: React.FC<ArticleProps> = ({ title, tag, content }) => {
return (
<div className="article">
<ArticleHeader title={title} tag={tag} />
<ArticleBody content={content} />
</div>
);
};
上記のように、使用時に、Article
の内部構造が隠蔽されてしまいます。
Article
がどのような子コンポーネントで構成されているかが、明確ではありません
Compound Patternのその他メリット
-
柔軟な構成:
- 必要に応じて、コンポーネントを自由に追加、削除、または並べ替えることができる
- これにより、異なるシナリオに対応しやすくる
-
デバッグの容易さ:
- コンポーネントの階層構造が明確なため、問題が発生した際にどのコンポーネントが関係しているかを特定しやすくなる
-
propsバケツリレーの回避:
- 深くネストされたpropsのバケツリレーを避けることができる
- カスタマイズの柔軟性:
- 必要なコンポーネントのみを使用し、不要なものを省略できる
Compound Patternを使わない時のデメリット
使う時のメリットの裏返しにはなりますが、複雑なコンポーネントでCompound Patternを使わない時に発生しうるデメリットです。
-
構造の固定:
- Articleコンポーネント内で構造が決定されているため、使用時に順序や構成を変更することが難しい
-
propsのバケツリレー:
- 親コンポーネントから子コンポーネントへpropsを渡す必要があり、中間コンポーネントを経由してpropsが伝播される
-
カスタマイズの制限:
- 各部分に対して個別のpropsや子要素を追加することが難しくなる
-
明示的な構造:
- 使用時に、Articleの内部構造が隠蔽されており、どのような子コンポーネントで構成されているかが明確でなくなる
デメリットは?
以下のようなデメリットがあるみたいです。
この辺りの話はまだまだ勉強中なので、別の機会に記事化しようと思います。
-
パフォーマンス最適化の難しさ
- コンテキスト依存性:子コンポーネントが親のコンテキストに依存
- プロパティ注入メカニズム:React.cloneElementによる動的プロパティ付与
- レンダリング連鎖:親の状態変化が子コンポーネント群全体に影響
-
React.Children.map()
やReact.cloneElement()
を使う時に複雑性や制限が出てくる- コンポーネント階層の制限
- プロパティ管理の複雑性