【React】フォルダ構成
はじめに
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で使用するreducer
とstore
をまとめています。説明省略。
types
全機能から参照される可能性のある型定義(type
)をまとめています。
例えば以下のようなものです。
- エラーメッセージ表示用の型定義
-
select
のドロップダウンに使うデータの型定義
また、列挙体や定数をまとめたものも格納します。
例えば、以下はColor
をプログラム内で使うために定数化したものになります。
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
は以下のコードのようになります。
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
を実装した例は以下のコードのようになります。
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時間程度。空き時間でやるにはボリュームが大きいですが、今年の目標にもしているので継続したいと思います。
Discussion