コンポーネント設計について自分用メモ
設計について考えをまとめるためのメモ
過去に書いた記事
これをアップデートしていきたいコンポーネント指向
コンポーネント指向、コンポーネント駆動と呼ばれる開発ではいくつかのコンポーネント群を組み合わせて、アプリケーションのUIを構築する。
そのため、独立した小さいパーツから作り上げていくボトムアップでの設計が重要。
参考
コンポーネントの性質
再利用可能
さまざまな場所で利用することができる
交換可能
他の同様のコンポーネントと自由に置き換えることができる
拡張可能
使う場所に併せて、新しい動作を拡張できる
カプセル化
呼び出し元はインターフェースの知識だけでコンポーネントを利用することができる
独立性
他のコンポーネントに影響を与えない
参考
コンポーネント指向で開発することで何がうれしいのか?
- 再利用できるコンポーネントが増えることでUIの開発工数を削減できる
- コンポーネントの影響範囲が予測しやすくなり修正や拡張が楽になる
- 再利用しているコンポーネントを改修する場合、コードの修正箇所などが削減できる
- UIを統一できる
再利用可能 - どのように作ればいい?
レイヤーごとに分割する
UIを複数のレイヤー(粒度)に分割することで再利用性は増す。
ただし、レイヤーを増やしすぎると管理が大変になる。
UIの一番低レイヤーといえるのはグルーバルなCSSやHTML要素?。
グローバルなCSSはどこでも再利用が可能なので、このレイヤーに独自の実装を増やしすぎると逆に影響範囲が予測しづらく管理が大変。
一番小さくても、HTML、CSS、UI独自の挙動をセットで1コンポーネントとして管理するのがいいと思う。
逆に Tailwind css や Bootstrap などはどこでも呼び出されることを前提にしていて、それ自体を頻繁に修正したり拡張したりしないのでグローバルなCSSでも問題ない。
有名なレイヤーの考え方としてはAtomic Designがあり、アプリケーションのUIを5つのレイヤーに分ける。
個人的にAtomic Designを以下のように解釈している。
- atoms
開発者が定義するコンポーネントのうち最小単位のコンポーネントで、これ以上分割出来ないもの。 - molecules
atomsのコンポーネントを複数組み合わせたコンポーネント。
いくつ組み合わせてもアプリケーションの機能として独立していなければmoleculesに分類する。 - organisms
アプリケーションとして独立した機能や意味を持つ単位。 - templates
ページの構成やレイアウトについて情報をもつコンポーネント。
複数のページで同じパーツ配置のページがある場合はtemplatesコンポーネントで共通化する。 - pages
ページ全体の情報をもつコンポーネント。
どのレイアウトでページを表示するかtemplatesから選んでpagesコンポーネントに持たせる。
ページ固有のコンテンツどのようなものにするかはorganismsにコンテンツを選んでpagesコンポーネントに持たせる。
Atomic Designはレイヤーの基本的な考え方として有益だけど、それをそのままディレクトリ構成やファイル分割ルールとして開発に適用できるわけではなさそう。
とくにReactでは1ファイルに複数コンポーネントを定義できるので、わざわざファイルやディレクトリを分割するのは運用コストが大きい。
逆にVueはシングルファイルコンポーネントなので、Atomic Designのレイヤーをそのままコンポーネントのルールに適用して、レイヤーごとにディレクトリを分けても割と運用しやすい。現在参加しているVueのプロジェクトでも問題なく運用できている。
個人的にatomsとmoleculesの差は、それらを構成するコンポーネントの数の単位なので、そこまで重要性を感じない。
どちらかというとドメインの情報をもっているか?
API呼び出しやグローバルステートのような副作用をもつか?
というのが明示的にわかることを重要視してディレクトリ構成を考えたほうがいいと思う。
HOC、コンテナーの扱い
APIやグローバルステートの利用とUIの実装は分離したほうが、
テストやストーリブックでの管理が容易になったり、UIの再利用が可能になる。
そのため、APIやグローバルステートの呼び出しをカスタムフックにまとめるか、コンテナーコンポーネントに副作用のあるロジックを置くことが多い。
ただ、小さいコンポーネントに副作用を持たせるためにコンテナーコンポーネントで囲んでも、
そのコンポーネントを含むを親コンポーネントでは子コンポーネント内の副作用を抱えることになってしまう。
これは、カスタムフックを作って小さいコンポーネントで呼び出すときも同様で、
ロジックの定義の場所を分割できても、コンポーネントが副作用を抱えてしまうことには変わりない。
なので、ロジックとUIの分割をすることとは別の意味として、副作用をどこのレイヤーに置くかということを意識したほうがいい。
個人的に、Atomic Design でいう Organisms 以上のレイヤーで API やグローバルステートを利用して、子コンポーネントにはプロップスでデータを渡すのが良いと感じている。
ただその分、プロップスのバケツリレーは必要。
Recoil や Jotai などグローバルステートを簡単に利用でき、レンダリングも考慮されているライブラリが増えているので、階層が多いコンポーネントでは末端のコンポーネントで直接必要なデータを取得したほうがいい場合もあるかもしれない。
交換可能 - どのように作ればいい?
インターフェースを統一する
異なるコンポーネントでも受け取るプロップスのルールや呼び出し方を出来るだけ統一することで、コンポーネントを交換してもバグが起こりにくくできる。
また、ルールが同じだとコンポーネントの利用者の学習コストを少なくでき、開発速度があがったり、間違った使われ方をすることを減らせる。
Reactの場合だと下記の感じ。
- クラス名の受け取りは
className
に統一する。 - イベントハンドラは
on + event name
で統一する。 - 真偽値を受け取るプロップスは
disabled
のような形容詞のみ、またはis + 形容詞
どちらかに統一する。
ここらへんのルールはHTML要素の属性のルールを真似したり、Material-ui や ChakraUI などを参考にすることで、共通認識として理解されやすいルールに寄せることができると思う。
また、コンポーネントのレイヤーや分類をわかりやすくすることもコンポーネントの交換可能性を高めることにつながる。
例えば、iconsディレクトリにアイコンコンポーネントをまとめておけば、そのディレクトリ内のコンポーネント同士であれば交換可能であることは予測することがしやすくなる。
もちろん、アイコンコンポーネント同士で利用方法を統一することは必須になる。
拡張可能 - どのように作ればいい?
クラス名やイベントハンドラは受け取れるようプロップスにする。
あるいはあとで、イベントハンドラなどのプロップスを追加できるようにしておく。
また、マストではないが高階コンポーネントでもプロップスを受け取れるようにして、外から渡すプロップスと高階コンポーネント内で取得するプロップスをマージできるようにしたほうが良さそう。