4.2K Views
November 26, 25
スライド概要
React Router v7とremix-flat-routesを活用したフロントエンドのコンポーネント設計戦略として、高い保守性を実現するためのガイドラインを提案しています。
その主要な概念は機能的凝集であり、これはモジュールを単一の明確な目的に集中させ、条件分岐がコード中に散らばる論理的凝集というアンチパターンを避けることを目指します。
この凝集度を高める考え方をコロケーション構成と組み合わせることで、ルートディレクトリが機能境界として明確に機能し、データ取得から更新に至るデータフローがディレクトリ内で閉じる構造が実現されます。
実践パターンとして、ユーザーロールや作成/編集機能に応じたルートの分離方法が具体的に示されており、共通化すべきコンポーネントは、3つ以上のルートで使用されるようになってからapp/features/などに切り出すべきであるという明確な基準も提供されています。
この設計により、変更の影響範囲が局所化され、システムの理解と拡張が容易になります。
機能的凝集とコロケーション 保守性の高いReact Router v7コンポーネント設計戦略 NotebookLM
そのコンポーネント、将来の変更に耐えられますか?
「購入者と出品者でUIはほぼ同じ」・・・この初期判断が、後に技術的負債となる。
// 避けるべき例: 論理的凝集
function ProductDetailPage({ role }:
{ role: "buyer" | "seller" | "admin" }) {
return (
<>
{role === "buyer" && <PurchaseButton />}
{role === "seller" && <StockEditor />}
{role === "admin" && <AdminPanel />}
{role !== "buyer" && <InternalNotes />}
{/* さらに条件分岐が増えていく... */}
</>
)
}
※論理的凝集の例: 異なる役割のロジックが単一コンポーネントに混在
コンポーネントの内部
buyer
seller
admin
条件分岐の複雑な絡み合い
条件分岐の複雑な絡み合い
役割ごとに散乱したロジック
予測不能な影響範囲
NotebookLM
問題の正体:論理的凝集(The Anti-Pattern) Definition: 関連性の低い要素が「カテゴリが同じ」という理由だけで一つのモジュールに まとめられた状態。 論理的凝集 無関係な要素の混在 複雑性 複雑性 あるべき姿へ あるべき姿 モジュールA (円形要素) モジュールB (方形要素) モジュールC (三角形要素) ● 条件分岐がコード全体に散らばる ● 一つの変更が予期せぬ箇所に影響する(副作用) ● 要件とコードの対応関係が不明瞭になる NotebookLM
我々が目指すべき姿:機能的凝集(The Ideal) Definition: モジュール内のすべての要素が「単一の明確な目的」のために連携している 状態。 購入者向け商品ページ レビュー 表示 購入ボタン 出品者向け商品ページ 在庫管理 公開設定 ● 購入者向け商品ページ: 購入者の機能しか含まない。 ● 出品者向け商品ページ: 出品者の機能しか含まない。 NotebookLM
機能的凝集がもたらす2つの絶大なメリット 1. 変更への強さ (Resilience to Change) ● 購入者機能の修正が、出品者機能に影響しない。 ● 関心事が分離され、コードの理解と修正が容易になる。 2. 要件との明確な対応 (Clear Requirement Mapping) ● 機能単位で書かれた要件定義書 とコードが1:1で対応する。 ● 開発者・デザイナー間のコミュニ ケーションが円滑になる。 NotebookLM
実装戦略:React Router v7 + コロケーション Core Idea: ルートごとにディレクトリを作成し、関連ファイル (route, components, services) をすべて同じ場所に配置する。 app/ routes/ _buyer+/ # 購入者向け ... _seller+/ # 出品者向け ... Technology Stack: ● React Router v7 (framework モード) ● remix-flat-routes NotebookLM
相乗効果:ディレクトリ = 機能境界 _seller+/products+/$productId+/ データフローの閉塞: loader → コンポーネン ト → action が同一ディ レクトリ内で完結する。 loader route.tsx action 影響範囲の明確化: _buyer+ 以下 の修正は、_seller+ に影響しない ことが構造的に保証される。 共通化の判断: 配置場 所に迷うコンポーネント は、共通化すべきではな いサインかもしれない。 NotebookLM
実践パターン①:ロールによる分割 レイアウトルート (`_buyer+`, `_seller+`, `_admin+`) でユーザー体験を完全に分離。 各ルート内にはそのロールに必要な機能だけを実装する。 購入者 _buyer+/.../route.tsx // app/routes/_buyer+/.../route.tsx export async function loader({ params }) { // 購入者向けのデータ取得 return { product: await getProductForBuyer(...) } } // action は存在しない 出品者 _seller+/.../route.tsx // app/routes/_seller+/.../route.tsx export async function loader({ params }) { /* ... */ } export async function action({ request }) { // 在庫更新などの出品者向け処理 /* ... */ } NotebookLM
実践パターン②:作成 (new) と編集 (edit) の分離 たとえUIコンポーネント (フォーム) が同じでも、責務が異なるためルートは分割する。 共通フォームは親の `_shared/` ディレクトリに配置し、各ルートから利用する。 app/routes/products+/ _shared/ components/ product-form.tsx <-- 共通フォーム new+/ route.tsx <-- action のみ $productId+/ edit+/ route.tsx <-- loader と action NotebookLM
実践パターン③:Outletによるネスト 親ルートで共通のデータ (例: 商品情報) を取得し、共通のレイアウトを定義する。 子ルート (詳細、レビュー、編集フォーム) は自身の機能とデータ取得に集中できる。 Parent Route (`$productId/route.tsx`) Product Header Navigation Tabs 詳細 レビュー 編集 <Outlet /> 子ルートコンポーネントが ここにレンダリングされる // app/routes/products+/$productId+/route.tsx (親) export default function ProductLayout() { return ( <> <ProductHeader /> <Tabs>...</Tabs> <Outlet /> </> ) } NotebookLM
例外対応:ルートで分割できない場合の機能的凝集
Scenario: 通知一覧など、単一リスト内で多様な要素を出し分けるケース。
Solution: `ts-pattern` を用いて、各ケースの処理を分離する。
function NotificationItem({ notification }) {
return match(notification)
.with({ type: "order_completed" }, (n) => <OrderNotification data={n} />)
.with({ type: "review_posted" }, (n) => <ReviewNotification data={n} />)
.with({ type: "stock_alert" }, (n) => <StockNotification data={n} />)
.exhaustive()
}
● 各通知タイプの処理が .with() ブロック内に閉じる
● exhaustive() によるコンパイル時の網羅性チェック
NotebookLM
共通化の判断基準:「いつ」「何を」「どこに」置くか 「似ている」と感じても、すぐに共通化しない。共通化してよいのは、 APIやDBスキーマで同じ型を参照しているなど、意味的に同一の場合。 共通化したいコンポーネントか? WHEN? (いつ) 2つ以下 3つ以上のルートで利用 共通化を避ける (Don't Repeat Yourself より Right Abstraction) WHERE? (どこに) 同一ルート内 → `components/` 親子ルート間 → 親の `_shared/` 複数機能横断 (3+ routes) → `app/features/` NotebookLM
ブループリント:理想的なアプリケーション構造 app/ features/ user/ components/ services/ routes/ _buyer+/ _seller+/ products+/ _shared/ components/ $productId+/ route.tsx components/ new+/ (3+) 複数機能で共通 (親子) 親子ルート間で共通 (同一) ルート内で共通 NotebookLM
Key Takeaways: 設計原則サマリー 1. 脱・論理的凝集: `role` propによる条件分岐から卒業する。 2. 機能的凝集を意識: 1ルート = 1機能の原則を徹底する。 3. コロケーションを活用: ディレクトリ構造をアーキテクチャの武器にする。 4. 共通化は慎重に: 「3回ルール」を適用し、早すぎる抽象化を避ける。 NotebookLM
Resources & Further Reading React Router v7 reactrouter.com remix-flat-routes github.com/kiliman/remix-flat-routes ts-pattern github.com/gvergnaud/ts-pattern Inspiration 機能的凝集の概念を用いて、複数ロール・類似機能を多く含むシステムの フロントエンドのコンポーネントを適切に分割する (Noritaka Ikeda氏) NotebookLM