🏢

【React】フォルダ構成

2023/04/27に公開

はじめに

React開発をする上で悩みの1つとなる「フォルダ[1]構成」。
別にテキトーでもシステム自体は動きますが、可読性や保守性を考えると考慮せざるを得ない要素の1つとなります。
特に、複数メンバーでの開発や、メンバーの入れ替わりが激しい現場においては「わかりやすく」「規則性があり」「間違えにくい」フォルダ構成が要求されます。

開発環境

Windows 10
Node.js 18.15.0
TypeScript 4.9.5[2]
React 18.2.0
React Redux 8.0.5
React Router 6.9.0
MUI 5.11.14
Visual Studio 2022

公開されている情報

公式サイト

公式サイトでは以下のようなパターンが紹介されています。

機能ないしルート別にグループ化する

プロジェクトを構成する一般的な方法の 1 つは、CSS や JS やテストをまとめて、機能別ないしルート別のフォルダにグループ化するというものです。

ファイルタイプ別にグループ化する

プロジェクトを構築する別の人気の方法は、例えば以下のようにして類似ファイルをグループ分けするというものです。

そして、最後に

考えすぎない

という言葉で締めくくられています。が、私は相当時間を費やしてしまいました。

GitHub

よく参考サイトとして紹介される「bulletproof-react」では、超簡単に書くと下図のような構成が紹介されています。

─ src
 ├ 共通モジュール
 └ 機能別フォルダ
   ├ 機能A
    ├ 機能B
    ・・・

複数の機能で使うモジュールは1箇所に、その機能でしか使わないモジュールは機能別に分けて管理する方法です。

検討した結果

使っているライブラリや、そのプロジェクトで実現したいこと、開発リーダーの意向・趣味等、様々が要因で結果が変わってくると思いますが、ひとまず下図のような構成で進めることにしました。

─ node_modules
─ src
 ├ components
 ├ routes
 ├ schemas
 ├ stores
 ├ types
 ├ utils
 └ features
  ├ App
  │ ├ compenents
  │ ├ hooks
  │ ├ stores
  │ └ types
  ├ 機能A
  │ ├ compenents
  │ │ ├ childrenA
  │ │ ├ childrenB
  │ │ ・・・
  │ ├ hooks
  │ ├ stores
  │ └ types
  ├ 機能B
   ・・・

1つずつ解説します。

node_module

npmライブラリが格納されています。

src

実際に使うのはsrc階層以下となります。

components

どの機能にも依存しない、汎用的な自作コンポーネントを格納します。
例えば以下のようなものです。

  • MUIの標準コンポーネントを拡張したもの
  • メッセージ表示
  • ダイアログ
  • データベーススキーマ(後述)を与えると自動でinput-typeが決まる自作コンポーネント

routes

React Routerでのルーティング規則をまとめています。説明省略。

schemas

データベーススキーマをまとめています。

stores

React Reduxで使用するreducerstoreをまとめています。説明省略。

types

全機能から参照される可能性のある型定義(type)をまとめています。
例えば以下のようなものです。

  • エラーメッセージ表示用の型定義
  • selectのドロップダウンに使うデータの型定義

また、列挙体や定数をまとめたものも格納します。
例えば、以下はColorをプログラム内で使うために定数化したものになります。

Color.ts
import { blue, grey, red, orange } from '@mui/material/colors';

export const Color = {
    TitleBackgroundColor: blue[100],
    DisabledBackgroundColor: grey[100],
    InputErrorBackgroundColor: red[50],
    InputWarningBackgroundColor: orange[100]
}

export default Color;

これがあると、色の値を定数値として呼び出せます。

const anyColor = Color.TitleBackgroundColor; // 実際には blue[100] がセットされる

features

機能ごとにフォルダを分け、その機能独自のモジュールを格納します。
また、それぞれに以下のサブフォルダを配置します。

components

レイアウトに関するモジュール。
拡張子.tsxのファイルが格納されます。
画面全体のレイアウト定義と、画面に配置する個々のコンポーネントを分けて配置します。
例えば、以下の構成を考えます。

用途 モジュール名 格納場所
画面全体のレイアウト定義 FeatureX components/FeatureX.tsx
要素① FeatureX_1 components/children/FeatureX_1.tsx
要素② FeatureX_2 components/children/FeatureX_2.tsx
初期化処理:initializeを定義 useFeatureX hooks/FeatureX.ts

このとき、画面全体のレイアウト定義:FeatureX.tsx は以下のコードのようになります。

components/FeatureX.tsx
import { useEffect } from 'react';
import { useFeatureX } from '../hooks/FeatureX';
import { FeatureX_1 } from '../components/children/FeatureX_1';
import { FeatureX_2 } from '../components/children/FeatureX_2';

export const FeatureX = () => {
    const hook = useFeatureX();
    
    useEffect(() => {
        hook.initialize(); // hooksで定義している初期化関数を実行
    }, []);
・・・
    return (
        <>
            <FeatureX_1 /> // childrenで定義しているコンポーネントを呼び出し
            <FeatureX_2 /> // childrenで定義しているコンポーネントを呼び出し
            ・・・・
        </>
    );
}

useEffectはあるにしても、このコード内で実際の処理は行わず、'処理の呼び出し'だけを行っている点を意識します。

hooks

処理に関するモジュール。
拡張子.tsのファイルが格納されます。
Reactのカスタムフックをイメージしていますが、API呼び出しなんかもここに書きます。
上述した初期化処理:initializeを実装した例は以下のコードのようになります。

hooks/FeatureX.ts
export const useFeatureX = () => {
    const initialize = () => {
        // 初期化処理
    }
    
    const module1 = async () => {
	await ... // API呼び出しや非同期処理もOK
    }
    
    const module2 = (param: number): boolean => {
        return param > 0; // 引数や戻り値があってもOK
    }

    return {
        initialize,
        module1,
        module2
    }
}
stores

stateを操作するモジュール。
拡張子.tsのファイルが格納されます。
今は以下の2つの処理をまとめています。

  • stateの初期値を定義
  • React Reduxのstoreで管理されているstateを更新するaction
types

その機能で使うオブジェクトの型定義。
拡張子.d.tsのファイルが格納されます。
stateはもちろん、型をもった独自のオブジェクトがある場合も、ここに型定義を記述します。

freatures/App

原則、featuresには機能別のモジュールを格納しますが、Appだけは共通っぽい内容になります。
主に以下の状態管理に関するモジュールをまとめています。

  • 登録系の画面における変更フラグ
  • 変更フラグがOnのまま画面遷移しようとしたときに表示する破棄確認メッセージ
  • React Routerのnavigateで↑が反応するよう拡張
  • ↑のメッセージ(ポップアップ)用のコンポーネント
  • React Reduxで管理するまででもないが、コンポーネントをまたいで参照したいパラメータを定義
  • ゲスト⇔ログイン⇔ヘルプの状態管理[3]

期待する効果

今回のフォルダ構成整備で以下の効果を期待しています。

  • メンバーが変な場所に変なファイルを勝手に置かなくなる
  • 個々の機能について、自然と「表示」「処理」「stateへの反映」を分けたコーディングを行う意識が高まる
  • カプセル化や、モジュール結合度・モジュール強度のような概念が自然と身につく(のか!?)
  • ↑によってバグが減る(のか!?)[4]

改善点

整備前は、1つのフォルダに大量のファイルがあり、さらに1つのファイルに複数の処理を記述していました。
「大量のimpotを書かなくても良い」「stateを好きな時に操作できる」「画面切り替えしなくても、スクロールだけで機能全体が読める」等、幼稚な考えではありますが楽な点もありました。
この点を、現状を維持したまま復元するには以下の改善が必要と考えます。

改善点 解決策 方法
大量をimportを書く羽目になる 参照先をまとめた定義ファイルを作る 各フォルダの直下にindex.d.tsを配置し、そのフォルダ以下でexportされているものをすべて記述する
import先のパスが../だらけになる 絶対参照に対応する 各機能から共通モジュールを参照するときは絶対参照形式でパス指定できるようにする
親から子へのstateのバケツリレーが生じる 子でしか使わないstateは親で持たず、子は子でstateを持つ Reduxのstoreで管理しなくて良いstateはローカル管理する。子の中で処理すべき処理はすべて子で完結させる。
階層が深くなりがち 頻繁に参照する処理はなるべく上層に格納し、下層への参照を減らす努力をする 左記のとおり
画面切り替えが激しすぎる 慣れる 何千行もスクロールするよりはマシと思う

以上です。

P.S.
これがZennの初投稿となります。
調べながらで3時間程度。空き時間でやるにはボリュームが大きいですが、今年の目標にもしているので継続したいと思います。

脚注
  1. 私の開発環境はWindowsなので「フォルダ」という表現を使います。 ↩︎

  2. 本当はV5系を使いたいのですが、create-react-appを使用しており、その内部で使われているreact-scriptがまだV5系に対応していないため、泣く泣くV4系となっています。 ↩︎

  3. ヘルプ状態は「ログインしていないがヘルプだけは参照できる」状態。ゲスト状態ではヘルプも参照できないようにしたかったための苦肉の策。 ↩︎

  4. なんとこのプロジェクトにはテストモジュールがありません。 ↩︎

Discussion