改めて考えるコンポーネントパターン
フロントエンド開発において何故か軽視されがちなのが、コンポーネントの切り方です。
コンポーネントって、個人的には脳死状態でも正しく切れるようにしておくべきだと思っていて、それくらいルールの整備が重要だと思っています。
今回は create-react-app などを用いた SPA を前提として話を進めていこうと思います。
まず、一番シンプルだと思われるコンポーネントパターンは、以下の形です。
├── src
│ ├── pages
│ │ ├── about
│ │ │ ├── index.tsx
│ │ │ └── style.module.scss
│ │ ├── blog
│ │ │ ├── date
│ │ │ │ ├── index.tsx
│ │ │ │ └── style.module.scss
│ │ │ ├── index.tsx
│ │ │ └── style.module.scss
│ │ ├── index.tsx
│ │ └── style.module.scss
│ └── index.tsx(ルーティング)
︙
まず、src
直下の index.tsx
で大枠のルーティングを行います。
pages
を含む各ページコンポーネントが、各 url のパスに割り当てられるコンポーネントとなります。
各ページコンポーネント内ではロジックが組まれると同時に、スタイリングを持つ形になります。
つまり、ルーティング用のコンポーネントを除き、各 url のパスに紐づくページコンポーネント以外のコンポーネントが切られることがないパターンになります。
メリットとしては、とにかくコンポーネントパターンが破綻することはないと思います。
デメリットとしては、とにかく各ページコンポーネントの肥大化がすごそうです。
上記の例では CSS Modules を想定しているので、スタイリング部分はコンポーネント側に乗りませんが、それでもえげつない行数になりそうですね。
加えて、コンポーネントの共通化が行われないため、コンポーネント志向の恩恵はかなり薄そうです。
フロントエンドの勉強を始めたばかりの人とかは、まずはこのようなパターンから勉強していくのが良いかもしれません。
次にシンプルなのはこんな感じですかね?
├── src
│ ├── pages
│ │ ├── about
│ │ │ ├── logic.ts
│ │ │ ├── index.tsx
│ │ │ └── style.module.scss
│ │ ├── blog
│ │ │ ├── date
│ │ │ │ ├── logic.ts
│ │ │ │ ├── index.tsx
│ │ │ │ └── style.module.scss
│ │ │ ├── logic.ts
│ │ │ ├── index.tsx
│ │ │ └── style.module.scss
│ │ ├── logic.ts
│ │ ├── index.tsx
│ │ └── style.module.scss
│ └── index.tsx(ルーティング)
︙
ファイル名が微妙ですが、最初のパターンからロジックを外出しにした形になります。
ロジックとスタイリングを別ファイルに切り出すという…書いていて思ったんですが、汚いパターンですね、これ。
とはいえ、いわゆる reselect
とか HOC をまとめる enhance
とかって、こんな感じのイメージですよね。
コンポーネント志向を理解していない Web デザイナーがプロトタイプを作ると画面単位でコンポーネントを切るので、そういったケースでは使えるかもです。
もちろん推奨はしないですが。
ただ、こちらも最初パターン同様、破綻はしなさそうですね。
次はコンポーネントの切り方がよくわからないフロントエンド初心者にオススメなパターンです。
├── src
│ ├── components(presentational components)
│ │ ├── Button
│ │ │ ├── index.tsx
│ │ │ ├── index.stories.tsx
│ │ │ └── style.module.scss
│ │ ├── Header
│ │ │ ├── index.tsx
│ │ │ ├── index.stories.tsx
│ │ │ └── style.module.scss
│ │ ├── Logo
│ │ │ ├── index.tsx
│ │ │ ├── index.stories.tsx
│ │ │ └── style.module.scss
│ │ └── Footer
│ │ ├── index.tsx
│ │ ├── index.stories.tsx
│ │ └── style.module.scss
│ ├── containers(container components)
│ │ ├── pages
│ │ │ ├── about
│ │ │ │ └── index.tsx
│ │ │ ├── blog
│ │ │ │ ├── date
│ │ │ │ │ └── index.tsx
│ │ │ │ └── index.tsx
│ │ │ └── index.tsx
│ │ └── index.tsx(ルーティング)
│ └── index.tsx
︙
上のパターンからネストを 1 段階深くしたパターンになります。
components
側はスタイリングを、 containers
側はロジックのみを持ちます。
いわゆる presentational components と container components ってやつですね。
Web デザイナーがコンポーネント志向は理解しているけれど、Atomic Design のようなコンポーネント設計を理解していないケースで有用です。
で、個人的には以下のルールの元開発を行うことを勧めます。
- components では一切のロジックを持たない(hooks や HOC の使用を禁止する)
- containers では一切のスタイリングを持たない
- components 以下のネストは 1 とする
このパターンだと、ある程度のルールを敷かないとすぐに破綻します。
一番破綻しやすいのが components 以下で、ここでフォルダのネストをしがちなのですが。
ネストを許容する場合、どういったルールの元にネストするかしないかきちんと決める必要が出てきます。
ここをきちんと決めていないプロジェクトがとても多く、結果コンポーネントをどこに切れば良いのかわからなくなり、プロジェクトの破綻につながっていきます。
あと、このパターンだと storybook や jest などの導入が楽です。
jest についてはどこまで導入するか多種多様な意見がありそうですが、少なくとも storybook は各 presentational component ごとに切れば良いので、保守性もそこまで悪くないと思います。
弱点としては、やはり components 以下にフォルダが大量に切られることと、presentational component の命名が難しそうです。
小さいプロジェクトであればこのパターンでも問題なく保守運用できそうですが、中~大規模なプロジェクトだと、全く太刀打ちできなさそうです。
最後は自分が最も使用するパターンです。
├── src
│ ├── components(presentational components)
│ │ ├── atoms
│ │ │ ├── buttons
│ │ │ │ └── Button
│ │ │ │ ├── index.tsx
│ │ │ │ ├── index.stories.tsx
│ │ │ │ └── style.module.scss
│ │ │ └── images
│ │ │ └── Logo
│ │ │ ├── index.tsx
│ │ │ ├── index.stories.tsx
│ │ │ └── style.module.scss
│ │ ├── molecules
│ │ │ └── navigation
│ │ │ ├── FooterNavigation
│ │ │ │ ├── index.tsx
│ │ │ │ ├── index.stories.tsx
│ │ │ │ └── style.module.scss
│ │ │ └── PrimaryNavigation
│ │ │ ├── index.tsx
│ │ │ ├── index.stories.tsx
│ │ │ └── style.module.scss
│ │ ├── organisms
│ │ │ └── global
│ │ │ ├── Header
│ │ │ │ ├── index.tsx
│ │ │ │ ├── index.stories.tsx
│ │ │ │ └── style.module.scss
│ │ │ └── Footer
│ │ │ ├── index.tsx
│ │ │ ├── index.stories.tsx
│ │ │ └── style.module.scss
│ │ └── templates
│ │ └── Layout
│ │ ├── index.tsx
│ │ ├── index.stories.tsx
│ │ └── style.module.scss
│ ├── containers(container components)
│ │ ├── pages
│ │ │ ├── about
│ │ │ │ └── index.tsx
│ │ │ ├── blog
│ │ │ │ ├── date
│ │ │ │ │ └── index.tsx
│ │ │ │ └── index.tsx
│ │ │ └── index.tsx
│ │ └── index.tsx(ルーティング)
│ └── index.tsx
︙
いわゆる Atomic Design ってやつですね。
構成としては、上のパターンの presentational components 側を Atomic Design に落とし込んだ形となります。
Web デザイナーがコンポーネント志向を理解していて、かつ Atomic Design もしっかりと理解しているケースで有用です。
このパターンのルールとしては以下がオススメです。
- components では一切のロジックを持たない(hooks や HOC の使用を禁止する)
- containers では一切のスタイリングを持たない
- components 以下のネストは以下の通りとする
- atoms, molecules, organisms は 2
- templates は 1
ただ、コンポーネント志向を理解されている方であれば、UI に関わるロジックを presentational components に乗せても問題ないと思います。
このパターンの大きなデメリットは 2 つ挙がります。
- props のバケツリレーが発生する
- SPA の場合 container components と templates の紐付けが Atomic Design の思想から外れる
ただ、バケツリレーについては個人的には全く気にしていなくて、むしろバケツリレーの何が悪いの?くらいに思っています。
container components と templates の兼ね合いについては結構難しくて、ここはプロジェクトごとに一定のルールを敷く必要があるかなと。
ちなみに、static なケースだと後者の問題は発生しません。
過去何度も書いてきましたが、本来コンポーネントパターンというのは Web デザイナーが理解する必要があるものであって、フロントエンドエンジニアは知らなくても問題はありません。
ところが、日本の現場では Web デザイナーがフロントエンドエンジニア以上に育っていないため、フロントエンドエンジニアがフォローせざるを得ないケースがほとんどです。
自身の経験上、Web デザイナーが在籍するプロジェクトに当たるほうが少なかったですし、コンポーネント志向や Atomic Design をきちんと理解している Web デザイナーやフロントエンドエンジニアには 1 人として出会ったことがありません。
いかにコンポーネントパターンやコンポーネント志向が軽視されているか、いかに Web デザイナーやフロントエンドエンジニアが舐められているか、その証明に他ならないよなーと思いつつ、今日このごろです。