Container/Presentationalパターンについて
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
コンポーネントに実装しています。
「データ取得」ボタンを押下した時に、関数fetchData
でfetch
を使ってデータを取得してきています。
そして、取得したデータをstate
管理しているので、セットしてPresentational
コンポーネントにprops
として渡しています。
そして、UI 実装については、役割ではないので、Container
コンポーネントはPresentational
コンポーネントを返すのみです。
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
で展開して表示しています。
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】よく耳にする設計用語の例え話をしてみた
関心の分離を意識した名前設計で巨大クラスを爆殺する
Discussion