Reactのディレクトリ構造パターン例

Next.jsをベースにWEBサイトを構築したのでどんな構成で構築したのかを備忘録として.

Reactのディレクトリ構造パターン例
Photo by Kimon Maritz / Unsplash

2021年に、Next.jsをベースにWEBサイトを構築したので、どんな構成で構築したのかメモしておきます。

ディレクトリ構成についてはいろんな意見があり、それぞれが強い意見を持っていると思います。僕もこれが一番いい、という趣旨ではなく、あくまで一つのパターンとして参考にしてもらえたらと思います。

今回の背景は以下の要素です。

  • 数人のエンジニアしかいない小さなスタートアップ、スピードの速い環境
  • PMFしていないため、捨てる可能性・変更する可能性がある

ディレクトリ構造と方針

今回、前提として極力アーキテクチャを入れない決定をしました。

その目的は「新規メンバーの参入障壁を下げる」ことです。プロジェクト初期に様々な方にアドバイスをもらったのですが、そのときに聞いた考えをまるっと参考にしました。

具体的には、DDDのようなアーキテクチャ論は用いず、ディレクトリ構造は一般的な構成+featureディレクトリ(+storybook)のような構成にしました。

Reactベストプラクティスの宝庫!「bulletproof-react」が勉強になりすぎる件

Reactの公式ドキュメントと近いのですが、feature(機能)は概念的に強く定義しませんでした。その代わり、そのチーム内でユビキタス言語を必ず定義し、そのワード(英単語)を共通認識としていたので、結果的にfeatureにはそのワードが頻出するようになりました。(このあたりはDDDの姿勢と近いので、≒DDDともいえるかもしれません。)

チームメンバーが非同期的かつ、フルリモート前提だったこともあるため、ルールは暗黙知とせずに極力eslintなどで縛るようにしました。

React+TSプロジェクトで便利だったLint/Format設定紹介

eslint-plugin-strict-dependencies

それ以外は極力ゆるく、何よりもスピード重視で都度やっていこ、のスタイルで組んでみました。

ディレクトリ構造のような答えの無いものは考え出すと時間が溶けてしまうので仮で進行し、問題が生まれてから考えるスタイルにしました。(WebStormのリファクタリング機能なら、後から変更してもほぼバグ無しでリファクタリングできるので・・・笑)

ディレクトリ

ちなみに公式では「機能・ルートによるグルーピング」と「ファイルタイプによるグルーピング」の2パターンの構成が紹介されています。

ファイル構成 - React

src

今回のsrcディレクトリは bulletproof-react と近い形になりました。

基本的にsrc直下はアプリケーション全体に依存するもの、それ以外はfeatures(機能に依存するもの)の分けです。

src
|
+-- components        # 共通コンポーネント
|
+-- constants         # URLなどのアプリケーション全体
|
+-- features          # feature based modules
|
+-- hooks             # 汎用的なhooks
|
+-- lib               # NPMライブラリに依存する汎用的な関数. wrapperなど
|
+-- pages             # Next.jsを採用したため. route
|
+-- routes            # routes configuration
|
+-- scheme            # zod schemeなど
|
+-- styles            # global style, colorなど。chakraのthemeなども
|
+-- types             # 共通のtype.d.tsなどをここに
|
+-- utils             # 汎用的なユーティリティ関数.

ストア管理は

  • 全体管理:Context
  • 部分管理:Recoil

にしました。

当初、Contextのみを使用して管理していましたが、機能単位でstate管理をしたくなり、都度Contextの実装をすることに手間を感じたので、コード量の少なさ・取り回しの良さを目的にRecoilを導入しました。

Contextは src/components/contextsに配置していますが、root(src)からの見通しが良いともいえないので、src/storesに配置してもよかったかもしれません。

features

src/features/awesome-feature
|
+-- api         # APIアクセスする主にPromise関数
|
+-- hooks       # feature依存hooks: apiファイルやformのstate管理など
|
+-- state       # 機能に依存するstate(Recoil)
|
+-- (etc)       # その他、機能に関するものすべて. converterなど

機能に関するものすべての感覚で入れていますが、主に api / hooks を中心に、store(state)を入れたりしてます。

components にfeaturesディレクトリが配置されているので、機能特有のパーツはこちらに配置されてます。

components

src/components
|
+-- context                  # 全体store. Context
|
+-- features/awesome-feature # 機能依存のComponents: 主にForm.tsxなどでfeaturesのhooksを呼び出す
|
+-- pages                    # Next.js(~v12)でpageコンポーネントを分割するため
|
+-- ui                       # UIパーツを配置。基本的にstorybookで構築できるようなプリミティブな値を引数にする。

複数の機能にまたがるようなグレーゾーンのものや、機能に依存するけど共通で呼び出しそうなものは機能名っぽくなくてもsrc/compoents/features/xxx/ に配置して疎結合なディレクトリを汚染しないようチームで心がけていました。

また、firebaseを採用しており、実装スピード面やパフォーマンス面から react-firebase-hooks をComponentから直接読んでいるものも許容し、同じく src/compoents/features に配置しました。

components/ui

src/components/ui
|
+-- (parts name)       # 各UIパーツ
|
+-- layouts            # FooterやHeaderなどパーツを組み合わせたもの

Atomic Designは実践的なやり方がいいんじゃないかと思っているクチで、Atomsのようなパーツだけしっかり分けておいてstoryも書いておいて、それを組み合わせたMolecules以上はparts以外、と分けておけばいいんじゃないかと考えています。

今回のデザインシステム上もパーツの定義ははじめにしっかり決まっており、それ以上は画面デザイン時に決める方針(あの画面と同じ構成で、etc…)だったので、チーム運用の兼ね合いとしてもベターだったと思います。


こんな感じで、1ページを構成するファイルの主な依存関係は以下の感じになりました。

eslint-plugin-strict-dependencies でその依存関係のルールはこんな感じです(tsconfigでpath設定している前提です)

// .eslintrc.json
{
	"strict-dependencies/strict-dependencies": [
      "error",
      [
        /**
         * Example:
         * Limit the dependencies in the following directions
         * pages -> components/page -> components/ui
         */
        {
          "module": "@/components/page",
          "allowReferenceFrom": ["@/pages"],
          "allowSameModule": false
        },
        {
          "module": "@/components/ui",
          "allowReferenceFrom": ["@/components/pages", "@/components/features"],
          "allowSameModule": false
        },

        /**
         * example:
         * Disallow to import `next/router` directly. it should always be imported using `libs/router.ts`.
         */
        {
          "module": "next/router",
          "allowReferenceFrom": ["src/lib/router.ts"],
          "allowSameModule": false
        },
        {
          "module": "@/*",
          "allowReferenceFrom": ["src/*"],
          "allowSameModule": false
        }
      ],
      // options
      {
        "resolveRelativeImport": false
      }
    ]
}
// tsconfig.json
{
	...
  "compilerOptions": {
    "baseUrl": "./",
    "paths": {
      "@/*": ["src/*"]
    }
  }
	...
}

考え方としては、末端の部分はしっかりロジック分離しておいて、中間層の置き場所だけ決めておこう、という感じです。中間層は階層わかんないし端っこ固めておけばどうにかなるっしょ、的な。


こんな感じです。

サンプルリポジトリまで用意できたらよかったのですが、あとで気が向いたら作って追記しておきます。

参考程度にどうぞ!