- はじめに
- Atomic Design (アトミックデザイン) とは
- 実装における課題感
- 1. 同レイヤー同士の import を許すか論争が勃発する
- 2. レイヤーが足りなくなる
- 課題を加味した上での設計
- 実装のイメージ
- app.js
- pages.js
- さいごに
はじめに
DROBE では React のコンポーネント設計に Atomic Design を取り入れていますが、特にデザインにおける設計と React における実装という目的の違いから、実装していると微妙に概念が取り入れ辛く、しばしば方針に迷う事があります
この記事では現在 DROBE でどのように Atomic Design と実装の整合性に折り合いをつけているのかを解説します
Atomic Design (アトミックデザイン) とは
Atomic Design はアメリカの Web デザイナーである Brad Frost 氏が提唱したデザインシステムの設計方法です
Atomic Design by Brad Frost
We're tasked with making interfaces for more users in more contexts using more browsers on more devices with more screen sizes and more capabilities than ever before. That's a daunting task indeed. Thankfully, design systems are here to help.
atomicdesign.bradfrost.com
それぞれのレイヤーの詳細な説明はここでは省略しますが Atomic Design では UI を 5 つのレイヤーに分解してデザインする事を提唱しています
5 つのレイヤーとは Atoms, Molecules, Organisms, Templates, Pages になります
実装における課題感
Atomic Design の思想をそのまま React の Component 設計に当てはめようとすると、様々な所で課題が出てきます。もちろん思想や運用によっては課題とならない事も多々あるとは思いますが、DROBE で実際に発生した課題は以下のようなものになります
1. 同レイヤー同士の import を許すか論争が勃発する
同じレイヤー間の import を許すべきか、という論争が勃発します
例えば Organisms は Organisms を import して良いのか、Molecules は Molecules を import して良いのか、という議論です
ちなみに私は同一レイヤー同士の import は許したくない派です (笑)
2. レイヤーが足りなくなる
再利用可能な Component という形で考えると、レイヤーが 5 つしか無いと往々にしてレイヤーが足りなくなります
例えば、ローディング中にクルクルが出るボタン、などを考えると、LoadingButton というものを Atoms として定義しがちですが、同様にクルクルが出る form input を LoadingInput というような形にしていくと、結局クルクルの部分は全ての Component に実装するハメになります
ここで同一レイヤー同士の import を許容すると Loading を Atoms として定義しつつ LoadingButton も Loadingainput も Loading に依存する、という形になりますが、同一レイヤーの import を許容しない場合はすぐにレイヤーが足りなくなるか、結局同じような実装を複数の Component に書く必要が出てきます
課題を加味した上での設計
上記のような課題を踏まえて DROBE では以下のようなルールと共に Atomic Design にレイヤーを 2 つ追加しています (Atoms の下に Quantum (量子) を、Molecule と Organisms の間に Compounds (化合物) を追加)
- 一番上に app.js があって、そこで react router の定義をする
- 同じレイヤーの component は import してはならない
- これをしなければいけない場合は component の設計を見直すべき
- Query (API) を呼んで良いレイヤーは Organism 以上とする
- React hooks を使って良いレイヤーは Compounds 以上とする
- 逆の言い方をするとMolecules 以下は state を持つべきではない
- Propos として受け取って良い型を定義する
- Atoms は string, boolean, numeric のみ
- Molecules は array も受け取れるが、その中身は string, boolean, numeric に限定する
- Compounds 以上のレイヤーはどんな型でも受け取れる
まとめると、それぞれのレイヤーの役割と制約条件は以下のようになります
Layer 名 | 目的 | 相互参照 | Query を呼んで良いか | hooks | props で受け取って良い型 |
---|---|---|---|---|---|
Pages | URL に対応させる React router の path が各 page にマッピングされる | 無し | ○ | ○ | |
Templates | header / menu などを定義 自身で Organisms などを実体化する事はしない | 無し | ○ | ○ | stringbooleanarray<any>numericany |
Organisms | Hooks を呼べる | 無し | ○ | ○ | stringbooleanarray<any>numericany |
Compounds | Molecule を複数使った構造だが hooks は呼ばない 任意の型を props として受け取れる | 無し | X | userState useCallback useEffect | stringbooleanarray<any>numericany |
Molecules | Atoms を複数持てる構造 props としては js の組み込み型のみを受け取る | 無し | X | X | stringbooleannumericarray<string>array<numeric> |
Atoms | html の生タグ (div, p img など) と react material ui のタグのみで構成される最小単位(quontum 除く) | 無し | X | X | stringbooleannumeric |
Quantums | これ以上分解できない最小単位特殊型ではあり、現在はアイコンのみ | 無し | X | X | |
実装のイメージ
全部で 7 レイヤーとなった際の react の app.js と Pages レイヤーの実装イメージは以下のようになります
app.js
<Route
path={path}
render={({ match }) => (
<Page path={path} />
)}
/>
app.js は基本的には routing を行う役割に限定されます (もちろん redux などの data store はここで設定しますが)
React router の path と Pages レイヤーのマッピングを行います
pages.js
Pages レイヤーの責務は以下になります
- どの Templates を使うかを選択する
- Templates の中身として何を使うか
- 端的に言うと Organisms を実体化するのは Pages の責務になります
// render
const renderContent = () => {
return (
<Organism props={"dummy"}/>
)
}
return (
<AppTemplate path={path}>
{ renderContent() }
</AppTemplate>
)
さいごに
DROBE における Atomi Design を活かした設計を紹介しました
実際に作っていると、何を Atoms にして何を Molecules にするかなどといった議論をレビューなどを通して行ない、チーム内のコンセンサスを作っていくという事が必須になりますが、それは Component といった階層構造を伴うフロントエンドの設計や実装において必要なプロセスであると考えています
DROBE でも今回ご紹介した Component 設計に至るまでに前身となる 2 つの React Project がありました (現在でも古い設計のままメンテされています)
Frontend は技術の移り変わりが激しいので、新しく React Project を作るたびに喧々諤々議論しながら設計を固めていくという所に面白みを感じています