Atomic Design、どうですか?
フロントエンドをやってる系の人たちはぶっちゃけしっくりきてないとおもいます。
僕を含めて、当初より「これ微妙やね(まぁ流行ってるし適当ではあるから使っておくか)」という見解の人はチラホラみかけました。
それもそのはずです。
朧げな記憶ではありますが、Atomic Designはそもそもプログラマーのために作られたメソッド……ではありません。
デザイナーの方が考案した、デザインシステム構築のためのフレームワークです(そうだよね?)。
Sketchでデザインコンポーネントを作成する際に(たしかSketchだったはず。懐かしいですね)、Sketch上のコンポーネントの再利用性を高めて使いやすくするためのものだったのです(だったはずです)。
それが「増えまくったReactコンポーネントをいい感じにフォルダわけしてえなあ」という需要と噛み合って流行ったと認識しています。
筆者のメインフィールドはReactですが、コンポーネントの表示上の見た目と実装や内包するDOMの複雑度はAtomic Designの方法論で抽象化できない、と当初より感じていました。
……じゃあなんで使ってきたの?かというと、他に有名な方法論もないので他のエンジニアにとってのわかりやすさ、教えやすさを重視した結果ですね。逃げです。
しかし、やはりAtomicDesignが持っている根本的な欠陥によって、毎回毎回メンバーの混乱を招いています。
いい加減、ここいらでReactに特化したコンポーネント分類の方法を考察してみましょう。
ちなみにすでに世間的に有名なやつがなんかあるとか知ってる場合は教えて欲しいです。
(フロントエンドにとっての)Atomic Designの問題点を整理する
デザイン上の抽象度とReact実装の抽象度に乖離がある
例えばAtomですらそうです。
インタラクションによって要素が増減するコンポーネントは、意味論的にAtomicですが忠実に従うならばMoleculesやOrganismsになりえるでしょう。
非常に原理主義的な解釈をすれば、SelectはMoleculesが適切になるような気がしますし、Selectから出てくるドラム内に任意のコンポーネントをレンダリングできる設計にした場合(anchorにしたりとか?)の抽象度はAtomic Designで説明し切れる範疇を超えているようにおもいます。
もっと色々と書こうとおもいましたが、もうこれに尽きるとおもいます。
現在主流のデザインツールはおそらくFigmaだとおもいます。
Figmaはコンポーネントとしての要素管理が可能で、Figma上のコミュニケーションをAtomic Designベースで行うのは非常に有用だとおもいます。
でもデザイナーの方で使ってる方はあまり見かけないので、なにか使いにくい理由でもあるのかもしれません。
AtomicDesignに頼らない、新しいコンポーネント分類の考え方
新しいものを考えるといったものの、筆者はこの手の仕組みのクオリティそのものにそこまで絶対的な価値があるとは考えていません。
大事なのは
- 生産性に対して無害であること
- 平易かつシンプルで運用の負担にならないこと
- 自転車置き場の議論を避けられる程度に網羅的なこと
だと考えています。
分類してみる
Pages: 複雑なワンオフ品コンポーネント
AtomicDesignでいうところのpagesやtemplatesのレベルにあたるものでしょうか。
ちょうど良いのでこれは「pages」と呼び、通常のコンポーネントとは完全に区別して扱いましょう。
これ以下で述べるすべてのコンポーネントを組み合わせて作り上げるもの、ということもできます。
アプリケーション中では特注のコンポーネントであり、ユーザーに体験を提供するレベルです。
言い換えればfeatureやトランザクションスクリプト、use case……なんでも良いですが、そういったビジネスロジックと事実上の1対1で結合するレベルのコンポーネントです。
再利用されることもあるかもしれないですが、その場合は「同じ機能が複数の場所に分散している」ということになり、アプリの仕様レベルでのミスが疑われます。
Nextなど、使うフレームワークにも左右されるでしょうが、通常はurlと一対一に対応しています。
ここの層に対するテストは、ユニットテストと呼ぶには不適当でインテグレーションテストという規模感になるはずです。
Contexual: AppのContext(あるいはStore)に依存した部品
これはある意味でPagesに似ていますが、あくまで汎用性がある部品としての体裁を保ったコンポーネントを指します。
ちなみに汎用性があることと、実際に色々なところで再利用することには大きな隔たりがありますが、仮に一箇所でしか使わないものでもcomposableに実装するのがReactの基礎的な思想です。
抽象的な説明だとわかりにくいとおもうので、以下に例を挙げます。
// これはPartsレベル const Card = (props: {title: string; content: string}) => { return( <div> <h4>{props.title}</h4> <div>{props.content}</div> </div> ); }; // これはContexual const UserCard = (props: {id: UserId}) => { const user = useSelectUser(props.id); return( <div> <h4>{user.name}</h4> <div>{user.bio}</div> </div> ); };
最初のCard
の実装は貴方のAppStateへの依存がありません(プロジェクトの性質次第ではCSS的な依存性はあるかもしれませんが)。
対して後者のUserCardはselector
への依存、つまりAppStateへと依存しています。
AtomicDesign的に考えれば両者はOrganisms(まぁ、つまりほんとはCardTitleとかに分割すべきなんでしょう)として一緒くたにされてしまうでしょう。
しかしフロントエンド的には大きな違いがあります。
近年はFragment ColocationやuseSWRなどの流行によって*1隅に追いやられている概念ですが、前者はPresentationalComponentで後者はContainerComponentに分類されます。
前者は簡単にテストを書ける、後者のテストはモックやmswによる非明示的な副作用を要求します。
そして、前者を使って後者を実装することはできますが、後者を使って前者を実装することはできません。
以上のことから、筆者はこの二つを明確に区別することを推奨します。
こちらもPartsと同様にAtomicDesign的なUI上のサイズや複雑性は無視します。
Parts: 独立性が高く、すぐにでも別プロジェクトで使えるくらい独立しているUI部品
いわゆる理想的なReactコンポーネントです。
実装の複雑性や、内包するDOMの量などは問いません。
例えばreact-selectやmodal、MUIなどに含まれるようなCardなどはそれぞれ別種の複雑さ、DOMの関係性を持ちます。
しかしこれらは各アプリケーションのビジネスとは切り離された抽象性を持ち、独立した再利用性を持ちます。
ゆえに簡単のため、全てを「部品(パーツ)」としてまとめます。
ここで注意したいのは、デザインや見た目が独特であっても、Componentとしてpureで、理論的に他のアプリに組み込んで問題なければそれはpartsです。
あくまで機械的に分類します。
AtomicDesign的なUI上のサイズや複雑性は無視しますが、超巨大プロジェクトでディレクトリが肥大化しすぎて意味不明になってしまった場合はこの内部でのみAtomicDesign(page抜き)を採用しても良いかもしれませんね。
しかしその規模になったらもはやchakra uiみたいにライブラリ化を検討すべきでしょう。
Ghost: DOMの描画を伴わない特殊なコンポーネント
さて、ちょっと変わったものがでてきました。
GhostとはHaskellなどで使われるPhantom Type(幽霊型)にインスパイアされた命名です。
Phantom Typeとは実行時に消滅することからそう名付けられましたが、このコンポーネントも仮想DOMのみの存在で、実際のレンダリング時には存在が消滅します。
しかしロジック自体は残留するのでGhost
としました*2。
自立したスタンド<幽波紋>あるいは死後強まる念みたいなもんだとおもいねえ。
UIとしてレンダリングされない以上、このコンポーネントをAtomicDesignで分類するのはかなり難しいでしょう。
このタイプの部品を大量に自作するチームがいればなかなかのReact巧者でしょう。
全く使わないチームもいるかもしれませんが、そういう特殊さゆえに分類しておく価値はあるでしょう。
例えば筆者が大昔に試作したリストを間引いて擬似的に無限スクロールやページングを簡単に作れるようにするコンポーネント(突き詰めると両者のロジックは抽象化できる点が多い)。
あるいは(次のReac19で陳腐化しますが)<head />
へのメタタグ追加に使われるReact-Helmet、また window.addEventListener('beforeunload', listener);
をuseEffect内で持たせてユーザーの引き留めを行えるようにするコンポーネントなどなど……
というわけで、こういった描画ではなくロジックを提供するコンポーネントを「Ghost」と呼ぶことにしましょう。
「描画ではなく機能を提供」ということから命名候補に:
- functional
- feature
- metaphysical
etcetc……をおもいつきましたが
- featureは通常アプリの機能を表現する
- functional conmponentは別の文脈がある
- metaphysicalはかっこつけすぎ
ということで、プログラミング的に馴染みのあるPhantomを彷彿とさせながらみることのないGhostをチョイスしました。
番外……SubComponent: それぞれがサブパーツを持てる
AtomicDesignなんかもそうですが
「実質的にも意味論的にも明らかに特定の場所でしか使われないコンポーネントがAtomsに大量に宣言されるねんけど」
という事態をまねきがちです。
例えば先の例で述べたCardTitleは、原理主義的にはAtomsですが別の場所(例えばArticle)再利用されることはありませんし、たまたま見た目が同じだからといってこれを別の関係ない場所で再利用するのは悪です。
例えば何度か例に使っているCardであれば
src/components/parts/Card/index.tsx
src/components/parts/Card/components/CardTitle
src/components/parts/Card/components/CardContent
- その他
というように配置できるでしょう。
露出の仕方をCard.Title
方式にするかimport {Card, CardTitle} from '~/components/parts/Card';
方式のどちらにするかはプロジェクトごとに統一した方がいいかもしれません。
まとめ
本稿ではAtomicDesignという「UIデザイン上の複雑性に着目した分類」を捨てて、よりフロントエンド的な観点から、
Appへの依存しておりFeatureとの結合度も高い
- Pages
Appへ依存していながらFeatureとは切り離された
- Contextual
Appへの依存度が低く、別プロジェクトへの再利用すら可能な
- Parts
DOMの描画を伴わない
- Ghost
あからさまに特定のコンポーネントへと従属して使われるものはサブディレクトリにいれる
- 特殊:Sub
という4 + 1分類を考案しました。
この分類は多分に筆者の好みが混入していますし、フロントエンドはプロジェクトごとに複雑さの乖離が激しいです(例えば筆者の主戦場であるto B SaaSのstate管理はとんでもなく複雑 & 肥大化する傾向にありますが、反面初回アクセスのパフォーマンス要求やレイアウトの多様性は低いです)。
また、CSSをどう管理するか?についても界隈のベストプラクティスがいまだ揺れ動いていることから言及していません*3。
これを読んでなんか違うな?とおもった人は最強のデッキを作ってこれと戦わせましょう。
こちらからは以上です。