Figma上では同じに見えるボタンが2つある。けれど片方はフォームを送信する <button> で、もう片方は別の画面へ移動する <a> です。見た目が揃っているという理由だけで1つにまとめると、propsに条件分岐が増え、分けておいたときより読みにくくなります。
不要な繰り返しを避けること、関心をきれいに分けること。どちらも読みやすいReactコードには欠かせません。それでも、短く分割されたコードが常に最善とは限りません。コンポーネントは見た目と振る舞いと状態から成り立っていて、そのうち一つが似ていても、残りまで同じとは限らないからです。
以下は、私がReactコンポーネントの分割を判断するときに見ている点です。
大きなコンポーネントから始める
まずはロジックとJSXを含む、大きなコンポーネントを作ります。API通信はCustom Hookに書く、といったチームのルールがあれば従いますが、それ以上の分割は急ぎません。
要件を理解しきる前に分割点を決めるより、PoCのように、まず要件を満たすコードを書く方が簡単です。実際に動くコードがあれば、責務の境界も見えやすくなります。大きなコンポーネントを後から分割する方が、細かく分けすぎたコンポーネントを再び統合するより容易です。
最初から完璧な設計は決められません。分割する根拠がないうちは急がず、あとで整える前提で大きなまま置いておきます。
困ったら分割を検討する
判断基準に迷ったら、実際に問題が起きているかを見ます。大きなコンポーネントによって、理解、変更、テスト、再利用のどれかが難しくなったときに分割を検討します。早すぎる分割は、かえってコードを複雑にし、誤った抽象化を生む可能性があります。
見た目と抽象化した振る舞いの整合性を確認する
Figma上では似ているものの、利用箇所によって振る舞いが異なるコンポーネントを考えます。この場合、共通化した振る舞い、つまりpropsの型が、すべてのケースへ無理なく適用できるかを確認します。
たとえば、コンポーネント内にボタンが1つあり、クリック時の処理を常に VoidFunction として受け取れるなら、共通化しやすいでしょう。型を合わせるために多数の条件分岐や例外が必要になるなら、見た目が似ていても別のコンポーネントとして扱う方が素直です。
テストのしやすさで分割を考える
コンポーネントを構成する見た目、振る舞い、状態は、それぞれ異なる方法で検証できます。見た目は目視やVRT、アクセシビリティテストで確認し、振る舞いは別のテストで確かめます。多くのテストケースは、そのどちらか一方に焦点を当てています。特にロジックのテストでは、Reactコンポーネントのレンダリング結果が重要でないこともあります。
振る舞いが複雑になったら、Custom Hookへ切り出すことでテストを分離しやすくなります。レンダリングせずにロジックだけを検証でき、テスト全体の保守もしやすくなります。
ただし、API呼び出しや単純な状態更新だけなら、私は無理に分割しません。MSW(Mock Service Worker)などでHTTPリクエストをモックし、コンポーネント全体をテストします。小さなロジックまで細かく分割すると、実装とテストの両方で管理対象が増えるためです。困っていないなら、大きなコンポーネントのまま扱う方がシンプルなこともあります。
近くにファイルを配置する
1つのコンポーネントだけで使うCustom Hookや内部コンポーネントは、利用元と同じディレクトリに置きます。たとえば Population の本体を population/index.tsx、専用のHookを population/useX.ts に配置します。
共通部品ではなく、そのコンポーネントに閉じた実装だと伝わりやすくなり、リファクタリングするときも関係するファイルをまとめて把握できます。
分割を先送りするという判断
分割の根拠は、たいてい後から出てきます。読んでも処理を追えない、変更するのが怖い、テストが書けない、同じものを別の画面でも使いたい。そのどれかが起きるまでは、大きなコンポーネントのままでかまいません。
今「悪く見えるコード」を残しておく方が、要件が見えてから正しい境界を引き直せます。