背景
- 普段 atomic design の考え方に基づいて pages, templates, organisms, molecules, atoms とディレクトリを切ってコンポーネントを実装しているが、このレイヤーごとに実装して良い責務(処理)・実装しない方が良い責務(処理)があるなあと思うことがある。
- チームメンバーが書いたコードをレビューする時、違和感を覚えて「こうした方が良さそうだなあ」と思っても、自分が普段どうしてそういう書き方をしているのか言語化できていないと指摘や説明ができないので整理してみた。
全体像
レイヤー | 責務 |
---|---|
pages | 基本的には Route コンポーネントの直下で呼ばれる API リクエスト/レスポンスのハンドリングを行う クエリパラメータの取得やリダイレクトなど router 関連の処理を行う redux や context に依存する値や関数を呼び出す template, organisms を呼び出す loading/error/empty 等の状態に応じて呼び出す子コンポーネントを切り替える 基本的にはCSSファイルを持たない |
templates | ロジックを持たない pagesコンポーネントからのみ呼ばれる 1ページあたり1回だけ呼ばれる DOMツリーの最も root に近い場所にレンダリングされる CSSファイルを持つ |
organisms | ドメイン知識を持つ ロジックを持つ場合は表示を制御するコンポーネントと分けて実装する CSSファイルを持っても良い |
atoms/molecules | 基本的にはドメイン知識を持たない 複数 organisms から参照される CSSファイルを持つ |
「ドメイン知識を持つ」とは?
- 目安としては、そのコンポーネント内で扱う変数の中に、ドメイン特有の型を持つオブジェクトが存在すると「ドメイン知識を持っている」ことになると思う。
「ロジックを持たない」とは?
- 状態や副作用を持たず、単に受け取った props を元に子コンポーネントに割り振ったり、 HTML/CSS の実装に集中したりすること。
- props から何らかの計算をする処理が入っていたとしても、そのコンポーネント内に state や副作用を持たなければセーフ。
pages について
- pages のレイヤーで API リクエストやルーティング関連の処理を行う。
- API リクエストの状態やユーザーの状態などを元に呼び出す template や organisms を切り替える。
- 基本的には Route コンポーネントの直下にレンダリングされる。
- ルーティング関連の処理やパラメータは pages コンポーネント内で処理することで、その下の子コンポーネントが router に依存しなくなるので再利用性を高めることができる。
- redux store や context に依存する処理を行う。
- その下の organisms 以下のコンポーネントが redux や context に依存しなくすることで、再利用性や単体でのテスタビリティを高めることができる。
- CSS ファイルは持たないことが望ましい
- (小声) organisms 以下のコンポーネントのマージンの設計などを間違うとどうしてもスタイルを記述したくなってしまう場面もあり、たまに目をつぶることはある(できればスタイルの記述は organisms 以下に閉じ込めたい)。
const CommentListPageContainer: React.FC = () => {
const { params } = useRouteMatch<topicId: string>();
const { isLoading, error, result } = useAPI();
const currentUser = useCurrentUser();
if (!currentUser.canViewComments) {
// 権限を持たないユーザーを別ページへリダイレクト
return <Redirect to="/" />;
}
const topicId = Number(params.topicId) || null; // URLパラメータから値を取得して子コンポーネントに渡す
const isEmpty = result && result.comments.length === 0;
// 状態に応じて表示するコンポーネントを切り替える
return (
<SomeTemplate title="View All Comments">
<LoadingHandler isLoading={isLoading}>
<ErrorHandler isError={error}>
{isEmpty ? <Empty message="no comment" /> : <CommentList topicId={topicId} comments={result.comments} />}
</ErrorHandler>
</LoadingHandler>
</SomeTemplate>
);
};
- 1つのページ内に複数のセクションがあって、それぞれ独立したロジックを持つ場合(例えばダッシュボード等)は、1つの page コンポーネントに全てのロジックを詰め込むと混雑してしまうため、 page の配下にロジックを実装する用の organisms を実装することもある。
const FooDashboardPage: React.FC = () => {
return (
<SomeTemplate>
<HogeContainer />
<FugaContainer />
</SomeTemplate>
);
};
const HogeContainer: React.FC = () => {
const { isLoading, result } = useAPI();
// organisms レイヤーなので内部で template を呼ばない
return (
<LoadingHandler isLoading={isLoading}>
{result && <HogeList hoge={result.hoge} />}
</LoadingHandler>
);
};
const FugaContainer: React.FC = () => { ... };
templates について
- ロジックを持たず、レイアウトの記述に専念する
- pagesコンポーネントからのみ参照され、1ページあたり1回だけ呼ばれる
- DOMツリーの最も root に近い場所にレンダリングされる
organisms について
- ドメイン知識を持つ
- ロジック(状態、副作用)を持つ場合は表示を制御するコンポーネントと分けて実装すると混雑しにくい。
- 表示を制御することに専念するコンポーネントの場合は event handler 内にロジックを書かず、 native event を prevent したのち親にイベントの発火を通知するのみを行う。
- 「クリックしたことをユーザー行動ログとしてサーバーに送信する」みたいな処理を event handler 内に直接書いてしまいがちだが、再利用性が下がるのでトラッキング用の処理はロジックを担当するコンポーネントに集約したい。
- CSSファイルを持つこともあるが、徹底された atomic design では余白の設計などがうまくいっていれば organisms レイヤーにCSSを記述することなく molecules/atoms を呼ぶだけで十分であるらしい。僕はちょっとそのレベルには到達できていない。
const LogicOrganism: React.FC = ({ items }) => {
const onClickItem = (itemId: string) => {
// ロジックを担当するコンポーネント内では native event に関知せず、イベント発火時に行いたい処理のみに特化する
sendUserAction('click-item', { itemId });
};
return <ItemList items={items} onClickItem={onClickItem} />
};
const ItemList: React.FC = ({ items, onClickItem }) => {
return (
<ul>
{items.map(item => (
<li
key={item.id}
onClick={e => {
e.preventDefault(); // native event の制御は表示担当のコンポーネント側で行う
onClickItem(item.id); // 具体的なロジックは書かず親コンポーネントにイベント発火の通知のみ行う
}}
>
{item.name}
</li>
))}
</ul>
);
};
atoms/molecules について
- 基本的にはロジックやドメイン知識を持たず、様々なページから参照されるパーツ群を実装するレイヤー。
- ボタン、ナビゲーション、フォームパーツ、テキストの装飾といった最小の粒度から、モーダル、ドロップダウンメニュー、ツールチップ等の粒度のものも含む。
- モーダルやドロップダウンメニュー、「全文を読む」の開閉状態といった state を持ちたいこともある。そのコンポーネント内に閉じていて、かつ外部から操作しない前提である場合はセーフということにしている。
境界・分類について
- どのパーツが atoms でどのパーツが molecules なのか判断するのは難しいので、境界の議論は基本的にはデザイナーさんにお任せすることにしている。
- デザイナーさんが使っているツール(figma, Zeplin 等)の中でコンポーネントが atoms/molecules など粒度別に管理されている(らしい)ので、その分類に従うことにする
- また atoms/ や molecules/ 直下のディレクトリ名(button, form, text, nav, tab, modal といったパーツの分類)やコンポーネント名もデザイナーさんに決めてもらうと良い。
- デザイナーさんがデザインツール上でコンポーネントやその分類に名前を付けているので、共通の名前で呼べるとデザインレビューの際などに便利。
あとがき
- たぶん他にもたくさんあるので思いついたら追加していく。