React / JavaScript・Typescript のコーディング規約について考える
定数ファイルに関わる部分を記事化しました。
type: "a" | "b"
のような enum-like な引数・Prop による分岐はやらない。連想配列で実装する。
- Case A: 論理的凝集(関数内切り替え) → 機能的凝集(元から別々に定義する)
- Case B:
バリエーション: それに対する値
で揃えてメリハリを
Case A. 論理的凝集 → 機能的凝集に
❌ DON'T: if/switchで分岐
const getSchema = (type: "password" | "phone", options: Options) => {
switch (type) {
case "password": {
// options を使う
return z.string()....
}
case "email": {
return z.string().email()
}
}
}
深刻度:高
異種(Heterogeneous)のモノなのにまとめられているのが問題
→引数・返り値が union 型になってしまう。「この "options" はどの type で使われるの?」
✅ DO: 連想配列を使って、別々に定義する
const schema = {
password: (options: Options) => {
return z.string()
},
email: () => {
return z.string().email()
}
}
バリエーション: それに対する値
で揃えてメリハリを
Case B: ❌ DON'T: if/switchで分岐
// type: "contained" | "outlined" | "text"
const style = type === "contained"
? ".contained"
: (type === "outlined")
? ".outlined"
: ".text"
- 深刻度:中
- (しょうがないけど)メリハリが無くて読みにくい
✅ DO: 連想配列になおす
const style = {
contained: ".contained",
outlined: ".outlined",
text: ".text"
}[type]
「準定数としての純粋な関数」と「取得する方法(hook, コンポーネントにからむ)」を分離する (文言を中央管理する場合など)
❌ DON'T: 取得してからそのデータを使う処理がまとめられている
(ついでにswitch/if 文も使われている)
const HeadTitle: React.FC = () => {
const router = useRoute()
// 対象のページの時だけ利用する
const { userId } = router.query
const { data: userData } = useUserData(
(router.pathname === "/users/[userId]")
? { userId }
: undefined
);
// 色々なページに対して、同じような条件式が書かれている
const text = (() => {
if (router.pathname === "/users/[userId]") {
return `プロフィール ${userData.Name} | 〇〇サイト`
}
// 色々なページに対して、同じような条件式が書かれている
})();
return (
<Head><title>{text}</title></Head>
);
}
情報が散逸しないように中央管理したいが、このような形になってしまうと、
- 同じ条件式が複数必要になったり、
- ページによって必要なデータが異なっている
- それも条件分岐して、
- マジックナンバーが沢山でてきて
...とカオスになってしまう。
✅ DO: 取得する方法を分離する
export const title = {
"users/[userId]": (userName?: string) =>
userName && `プロフィール ${userName} | 〇〇サイト`
}
const UserProfilePage: NextPage = () => {
const router = useRoute()
const { userId } = router.query
const { data: userData } = useUserData({ userId });
return (
<>
<Head>
<title>{title["users/[userId]"](userData.name)}</title>
</Head>
{ /* ページのコンテンツ */ }
</>
);
}
「条件分岐」する必要は無くなり、「データの取得」という責務がページ側に移り、定数ファイルは渡されたデータをもとに文言を生成する純粋関数だけを管理すれば良くなり、コードが少なくなる。
ドメイン知識の有るコンポーネント/無いコンポーネントを区別する
深刻度:低 (ただし、一般化の漏れ・仕様変更で火を吹く可能性あり)
特に「何らかの情報を表示するためのラベル」のように、
定数通りの状態があって、それに応じて色・文言が切り替わる場合、
- 自由度のある atom/molecule
- 定数通りの状態に絞り込んだ特化(specialized)コンポーネント
に分ける
ディレクトリ構造:
- src/components
- ドメイン知識を持たない
Label
-
color
,text
,icon
Propを持つ
- src/features/books/components
- Books 機能のドメイン知識を持つ
BookStatusLabel
- <-
Label
の specialized 版 -
value: "available" | "reserved" | "archived"
だけで color, text, icon が定まる。
画像は自作
元ネタ
「見かけ駆動」なパッケージングをしない。
ディレクトリ分類のときは、hook, util, const, component, type のように漫然と分類しない。
「関心事ごと」「機能ごと」にパッケージングするべき。もし1つのパッケージが大きくなりすぎたら、さらに小さなパッケージに分ける。
もしあなたが「〇〇という小機能」について書かれているファイルを探すとき、「〇〇はHookによって実現されているから...」のような探し方をするだろうか?
否、「"〇〇"そのもの」あるいは「大項目 > 中項目 > 〇〇」...のように探すであろう。
Hookなのか、純粋関数なのか、コンポーネントなのか...というのは、あくまでその小機能を実現するための手段でしかないからだ。
特に、render hooks パターン、context など、複数の「手段(例: hookとcomponent)」が互いに緊密に連携するケースがあるので、「手段」ベースのパッケージングをすると、同じ関心事に関する記述がアチコチに散らばることになる。
下図のように、「関心事ごと」「機能ごと」にパッケージングするべき。
例:
- feature
- ユーザー
- ユーザー一覧ページ
- ユーザー検索UIの状態を保持する context
- ユーザー一覧ページ
- ユーザー
- 横断的関心事・中央管理したいデータ
- 文言
- バリデーション関連
- API呼び出し
- ドメイン知識を持たないコンポーネント(atom/molecule)
定数ファイルについても同様
「数値型だからnumbers.tsにまとめて置いたろ!」では、何がどこに置いてあるか分からない。
以下のように、それが何に関心がある定数なのかを見て分類するべき。
- ページネーションの件数
- 文字入力の最大文字数
- enumとしての整数定数 -> 型ごとにそれぞれまとめる
デフォルト値の定数ファイル外への漏出を避ける
❌ DON'T 呼び出し側がデフォルト値を参照する
- 定数が使われる箇所が一覧できず、grepする必要がある。
- 定数を使う側のコードに知識が漏れている
- 「Aの箇所ではデフォルト値を使って、Bの箇所では固有の値を使って...」
特に、{ maxLength: number, message: string }
みたいに、1つの物事について複数の要素が定数化されている場合に煩雑になってしまう。
const paginationSize = {
default: 20,
"articles/[id]/history": 100,
} as const
✅ DO 呼び出し側は「関心事の名前」だけで参照できる。
- デフォルト値を使うかどうかの知識が使用する側に漏れない
- 「Aの箇所ではAのための値、Bの箇所ではBのための値...」と素直に参照すればよい
-
全ての使用箇所が列挙されるので、定数ファイルを見るだけで一覧できる
- しかも、デフォルト・固有値それぞれの使用箇所が一覧できる
const defaultValue = 20
const paginationSize = {
"toppage/articles": defaultValue,
"articles/[id]/history": 100,
} as const