React の Component 設計と Atomic Design
React の Component 設計と Atomic Design

React の Component 設計と Atomic Design

はじめに

DROBE では React のコンポーネント設計に Atomic Design を取り入れていますが、特にデザインにおける設計と React における実装という目的の違いから、実装していると微妙に概念が取り入れ辛く、しばしば方針に迷う事があります

この記事では現在 DROBE でどのように Atomic Design と実装の整合性に折り合いをつけているのかを解説します

Atomic Design (アトミックデザイン) とは

Atomic Design はアメリカの Web デザイナーである Brad Frost 氏が提唱したデザインシステムの設計方法です

それぞれのレイヤーの詳細な説明はここでは省略しますが Atomic Design では UI を 5 つのレイヤーに分解してデザインする事を提唱しています

https://atomicdesign.bradfrost.com/chapter-2/

5 つのレイヤーとは Atoms, Molecules, Organisms, Templates, Pages になります

実装における課題感

Atomic Design の思想をそのまま React の Component 設計に当てはめようとすると、様々な所で課題が出てきます。もちろん思想や運用によっては課題とならない事も多々あるとは思いますが、DROBE で実際に発生した課題は以下のようなものになります

1. 同レイヤー同士の import を許すか論争が勃発する

同じレイヤー間の import を許すべきか、という論争が勃発します

例えば Organisms は Organisms を import して良いのか、Molecules は Molecules を import して良いのか、という議論です

image

ちなみに私は同一レイヤー同士の 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 (化合物) を追加)

image
  1. 一番上に app.js があって、そこで react router の定義をする
  2. 同じレイヤーの component は import してはならない
    • これをしなければいけない場合は component の設計を見直すべき
  3. Query (API) を呼んで良いレイヤーは Organism 以上とする
  4. React hooks を使って良いレイヤーは Compounds 以上とする
    • 逆の言い方をするとMolecules 以下は state を持つべきではない
  5. Propos として受け取って良い型を定義する
    • Atoms は string, boolean, numeric のみ
    • Molecules は array も受け取れるが、その中身は string, boolean, numeric に限定する
    • Compounds 以上のレイヤーはどんな型でも受け取れる

まとめると、それぞれのレイヤーの役割と制約条件は以下のようになります

レイヤーの役割

Layer 名目的相互参照Query を呼んで良いかhooksprops で受け取って良い型
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 を作るたびに喧々諤々議論しながら設計を固めていくという所に面白みを感じています