UIの情報構造に合わせたReact Componentを作る、UI Structureパターン
はじめに
ReactのComponentを、見通しよく開発しやすい形で設計するパターンを整理しました。
自分がUI Structureパターンと呼んでいるこの手法では、UIの情報設計として存在する要素分だけReact Componentを作ります。
ReactのデザインパターンはContainer/Presentationalパターンが有名ですが、Container/Presentationalパターンではロジックに関わるものを担うContainer Componentと見た目に関わるものを担うPresentational Componentにわけ、再利用性を高めています。
しかしUI StructureパターンではContainer Componentを作らずhooksやlogicsで切り出して対応します。
とはいっても、Reactの開発に慣れている方にとっては特別目新しいものではないかもしれません。
UI Structureパターン
UI StructureパターンではReact Componentを「UI要素を表現したもの」と捉えます。
ComponentはDataをもち、Dataを操作でき、ユーザ操作によって状態を変化させます。
ここからComponentが受け取るpropsの種類は以下の3つに限定されます。
- Data
- Data操作を行うhandler (onCreate, onUpdate, onDelete)
- 状態変化に伴うhandler (onActive, onOpen, onClose)
Dataはその要素がUIの情報設計として持っているであろうもののみを含み、他の情報はcontextを用いて渡します。
このパターンを用いるメリット
- Component構造の見通しが良くなる。構造がUIデザインと一致しているため、まずデザインを見ればコードの構造も把握できる。エンジニアとデザイナーでコンポーネント名を共通して使える。
- propsがシンプルになる。渡す数が少なくなるだけでなく、どういうものを渡すのかがはっきりする。バケツリレーを防止することができる。
例:Article
以下のような記事を表示することを考えてみましょう。
この記事は以下のような構造でUIが組み上げられています。
UI構造に合わせてReact Componentを切っていきます。
各コンポーネントに渡すDataは、コンポーネント名に一致させるようにします。
▼Article
index.tsx
▼Supplement
index.tsx
Info.tsx
Options.tsx
▼Body
index.tsx
type Props = {
artile: ArticleT
};
export const Article: React.FC<Props> = ({ article }) => {
return (
<div>
<Supplement supplement={article.supplement} />
<Body body={article.body} />
</div>
);
};
Supplement
の中にはブックマークボタンがあり、ブックマークするにはarticle.id
が必要です。
しかしSupplement
に直接article.id
を渡してはいません。Supplement
というComponentは article.supplement
という情報を扱うためのものだからです。
article.id
を渡すためにはcontextを使います。
type Props = {
artile: ArticleT
};
export const Article: React.FC<Props> = ({ article }) => {
return (
<div>
<ArticleIdContext.Provider value={article.id}>
<Supplement supplement={article.supplement} />
<Body body={article.body} />
</ArticleIdContext.Provider>
</div>
);
};
こうすることによって、Componentの階層構造をシンプルに保つとともに、propsを最小限に抑えることができます。
例:Articles
次に記事一覧を表示することを考えてみましょう。
この一覧コンポーネントでは、記事を追加できたり、各記事を削除することができます。
これを表示させるComponentのArticles
は以下のようになるでしょうか。
▼Articles
index.tsx
▼hooks
useArticles.ts
▼ArticleCreator
index.tsx
▼Article
index.tsx
export const Articles: React.FC = ({ }) => {
const { articles, onCreateArticle, onDeleteArticle } = useArticles();
return (
<div>
<ArticleCreator onActive={onCreateArticle} />
<ul>
{articles.map((article) =>
<li key={article.id}>
<Article article={article} onDelete={() => onDeleteArticle(article.id)} />
</li>
)}
</ul>
</div>
);
};
Article
の追加・削除に必要なハンドラはhooksで切り出し、それを加工しながら子供に渡してあげます。
ArticleCreator
に対してはonActive
で記事作成ハンドラを渡しています。onClick
は「状態変化に伴うhandler」とは言えないので、onActive
という名前にしています。
ここで、記事のタイトルを編集できる機能を追加しようとしたらどうすればいいでしょうか。
方法の1つはAPIリクエストを行うonUpdateTitle
を作って、Title
内でそれを呼ぶことですね。
もう1つの方法はonUpdateArticle
を作ることです。
タイトル以外にも編集できるものが複数ある場合にAPI1つで対応しようとすると、こういった形になります。
export const Articles: React.FC = ({ }) => {
const { articles, onCreateArticle, onUpdateArticle, onDeleteArticle } = useArticles();
return (
<div>
<ArticleCreator onActive={onCreateArticle} />
<ul>
{articles.map((article) =>
<li key={article.id}>
<Article
article={article}
onUpdate={(newArticle) => onUpdateArticle(artcle.id, newArticle)}
onDelete={() => onDeleteArticle(article.id)} />
</li>
)}
</ul>
</div>
);
};
type Props = {
article: ArticleT,
onUpdate: (newArticle: ArticleT) => void,
onDelete: () => void,
};
export const Article: React.FC<Props> = ({ article, onUpdate, onDelete }) => {
return (
<div>
<Title
title={article.title}
onUpdate={(newTitle) => onUpdate({ ...article, title }} />
</div>
);
};
どのComponentも「1.Data」「2. Data操作を行うhandler」「3. 状態変化に伴うhandler」以外のpropsを受け取らずに表現できています。
ありそうな議論
再利用性が低いじゃないか
フロントエンドの要素は変化しやすく、かつ場所によって差分が発生しやすいので、再利用するのは慎重に行うべきです。
基本的には再利用可能なComponentは小さい部品に限られると思っていて、それらはデザインシステムとして外部ライブラリに出してしまう方が設計がすっきりします。
Storybookが使いにくいじゃないか
Container/Presentationalパターンに比べてデメリットになっているのはこの部分ですね。ここは悩ましい箇所です。
ただモックサーバで対応することもできるし、見通しが良くなることで高速で開発しやすくなり、かつ組織のスケールアウトに対応しやすいという点で、このデメリットを上回るメリットがあると考えています。
Discussion
この記事の内容、特に「再利用性が低いじゃないか > 再利用するのは慎重に行うべきです。」のところは、私も全面的に賛成です。
蛇足かもしれませんが、React の公式ドキュメントにもそのような旨の説明があるので、よかったら実装方針の議論などでお使いください。