⚙️

Container/Presentationalパターンについて

2024/12/21に公開

Code Polaris Advent Calendar 2024 20 日目の記事です!

アドベントカレンダーでは、毎年その年に経験したこととかについて書くようにしているのですが、今年は「Container/Presentational パターン」です!!

Container/Presentational パターン

Container/Presentational パターンとは

まず、Container/Presentational パターンの概要です。

Container/Presentational パターンは、「関心の分離(Separation of Concern)」を目的とした、React のデザインパターン(設計手法)の 1 つです。
コンポーネントを、Containerコンポーネント と Presentationalコンポーネントに分ける設計です。

それぞれのコンポーネントの役割

コンポーネントが担う役割として、主に以下の 2 つがあると思います。

  • データの表示
  • ふるまいの定義

Container コンポーネントの役割

Containerコンポーネントは、表示するデータやふるまいをPresentationalコンポーネントや他のコンポーネントに渡す役割を担います。
つまり、「何の」データを表示するかや「どんな」挙動・処理をするかに関心を持つコンポーネントです。

例を挙げると以下のような役割をします

  • API 通信をしてデータを取得
  • ユーザーのアクションによって発火する関数を実装
  • その他ロジックの実装

Presentational コンポーネントの役割

Presentationalコンポーネントは、データを表示する役割を担います。
つまり、「どのように」データを表示するかに関心を持つコンポーネントです。

例を挙げると以下のような役割をします

  • 表示するデータを受け取って UI 構築

Containerコンポーネントで表示したいデータや挙動・ロジックを定義し、Presentationalコンポーネントに渡して表示するという設計になります。

個人的に感じるメリット・デメリット

色々教科書的なメリット・デメリットは他の記事でも見れるので、ここでは個人的な所感を。

メリット

  • 役割がはっきり分けられているので、コードが読みやすくなる(可読性)
  • 修正範囲などわかりやすい(保守性)

デメリット

  • コンポーネントの規模感によっては、やりすぎ感がある
    • Presentationalパターンの中でもいくつかのコンポーネントに分けることもありますが、その全部でContainer/Presentationalパターンをするのか...とか。

実装してみる

Container/Presentational パターンを使ってサンプル実装をしてみます。
以下が実装する要件です。

  • 「データ取得」ボタン押下で ToDo を取得してくる
  • 取得してきたデータを表示する

先に見せちゃうと、これが完成形です。

Container コンポーネント

Containerコンポーネントは、ロジックを実装し、Presentationalコンポーネントに渡す役割を担います。
「データ取得」ボタンを押下した時の処理・表示したいデータを取ってくる処理は、ロジックに該当するので、こちらのContainerコンポーネントに実装しています。
「データ取得」ボタンを押下した時に、関数fetchDatafetchを使ってデータを取得してきています。
そして、取得したデータをstate管理しているので、セットしてPresentationalコンポーネントにpropsとして渡しています。

そして、UI 実装については、役割ではないので、ContainerコンポーネントはPresentationalコンポーネントを返すのみです。

ToDoList.tsx
import { useState } from "react";
import { ToDoListPresenter } from "./ToDoListPresenter";
import { ToDo } from "../../types/todo";

export const ToDoList = () => {
  const [toDoList, setToDoList] = useState<ToDo[]>([]);

  /**
   * データ取得処理(ロジック)
   */
  const fetchData = async (): Promise<void> => {
    try {
      const response = await fetch(`${import.meta.env.VITE_API_BASE_URL}/todo`);
      const jsonResponse = await response.json();
      const responseArray: ToDo[] = [];
      responseArray.push(...jsonResponse);
      setToDoList(responseArray);
    } catch {
      throw new Error("Failed to fetch data.");
    }
  };

  return <ToDoListPresenter toDoList={toDoList} onFetchData={fetchData} />;
};

Presentational コンポーネント

Presentationalコンポーネントは、データを受け取って表示する役割を担うコンポーネントです。
Containerコンポーネントから propsとしてタスクの配列toDoListを受け取り、mapで展開して表示しています。

ToDoListPresenter.tsx
import { ToDo } from "../../types/todo";

interface ToDoListPresenterProps {
  /** タスク配列 */
  toDoList: ToDo[];
  /** データ取得処理 */
  onFetchData: () => Promise<void>;
}

export const ToDoListPresenter = ({
  toDoList,
  onFetchData,
}: ToDoListPresenterProps) => {
  return (
    <>
      <button onClick={onFetchData}>データ取得</button>
      <ul>
        {toDoList.map((toDo) => (
          <li key={toDo.id}>{toDo.title}</li>
        ))}
      </ul>
    </>
  );
};

このように、ContainerコンポーネントとPresentationalコンポーネントでそれぞれ、ロジックと表示と言った役割分担をする設計が、「Container/Presentational パターン」です。

おわりに

今回は、Container/Presentationalパターンについてでした!
要件によってはどっちに書くのか迷う時もありますが、可読性が上がる設計です。
改修などでも、「どこを見れば良いのか」がはっきりしているなと感じます。
プロジェクトによっては、規則などなくフロント実装しているところもありますが、Container/Presentationalパターンのようにコンポーネント設計方針があるだけで、実装自体もしやすくなるし、実装方針が統一されていることで後々の保守においても活きてくるなぁと感じています。

参考資料

Container/Presentational Pattern
Presentational and Container Components
【React】よく耳にする設計用語の例え話をしてみた
関心の分離を意識した名前設計で巨大クラスを爆殺する

GitHubで編集を提案

Discussion