
Container-Presenter Pattern(または Smart/Dumb Components パターン)は、
UIロジックと表示ロジックを明確に分離するための設計パターンです。
ReactやVueなどのコンポーネント指向UIでよく使われています。
基本概念
1. Container(Smart Component)
・状態管理(state)・データ取得・ビジネスロジックを担当
・API呼び出しやイベント処理など、「アプリの頭脳」部分
・UIは直接描画せず、データをPresenterに渡す
2. Presenter(Dumb Component)
・データを受け取り、表示だけを担当
・propsとしてデータを受け、イベントをコールバックで返す
・ロジックは持たず、見た目の責任だけを負う
データフロー構造
[Container] ← 状態を管理(Smart)
useState/useEffect
handleEvent()
↓ props
Presenter UIのみ担当(Dumb)
render(data)
onClick()
サンプルコード1 : React風
// Container Component(Smart)
function CounterContainer() {
const [count, setCount] = React.useState(0);
const handleIncrement = () => setCount(c => c + 1);
const handleDecrement = () => setCount(c => c - 1);
// Presenter に state とイベントを渡す
return (
<CounterPresenter
count={count}
onIncrement={handleIncrement}
onDecrement={handleDecrement}
/>
);
}
// Presenter Component(Dumb)
function CounterPresenter({ count, onIncrement, onDecrement }) {
return (
<div style={{ textAlign: 'center' }}>
<h2>Container-Presenter Pattern</h2>
<div style={{ fontSize: '2em' }}>{count}</div>
<button onClick={onIncrement}>+</button>
<button onClick={onDecrement}>-</button>
</div>
);
}
サンプルコード2 : Vanilla風
Reactなしで「状態管理とUI描画の分離」を体験できる構成バージョンです。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>Container-Presenter Pattern (Vanilla JS)</title>
<style>
body {
font-family: sans-serif;
text-align: center;
padding-top: 50px;
}
.count {
font-size: 2em;
margin: 10px;
}
button {
font-size: 1.2em;
margin: 5px;
padding: 5px 15px;
}
</style>
</head>
<body>
<div id="app"></div>
<script>
/**
* Presenter(UI担当)— Dumb Component
* @param {Object} props - count, onIncrement, onDecrement
*/
function CounterPresenter(props) {
const container = document.createElement('div');
const title = document.createElement('h2');
title.textContent = 'Container-Presenter Pattern (Vanilla JS)';
const countDisplay = document.createElement('div');
countDisplay.className = 'count';
countDisplay.textContent = props.count;
const incBtn = document.createElement('button');
incBtn.textContent = '+';
incBtn.addEventListener('click', props.onIncrement);
const decBtn = document.createElement('button');
decBtn.textContent = '-';
decBtn.addEventListener('click', props.onDecrement);
container.append(title, countDisplay, incBtn, decBtn);
return container;
}
/**
* Container(状態管理担当)— Smart Component
* 管理ロジックと再描画処理を担当
*/
function CounterContainer(root) {
let count = 0;
function render() {
root.innerHTML = '';
const presenter = CounterPresenter({
count,
onIncrement: () => {
count++;
render();
},
onDecrement: () => {
count--;
render();
}
});
root.appendChild(presenter);
}
render();
}
// 初期化
const app = document.getElementById('app');
CounterContainer(app);
</script>
</body>
</html>
ポイント解説
CounterContainer()
・状態(count)を持つ
・イベントハンドラを定義
・render()関数でPresenterを再生成して描画
CounterPresenter()
・DOM操作を担当
・props(状態とイベント)を受け取り、表示専用
メリット
・責務の分離:UIとロジックを完全に分離できる
・テスト容易性:PresenterはUIスナップショットテストだけで済む
・再利用性:同じPresenterを複数のContainerで使える
・保守性向上:データの流れが明確になる
デメリット
・小規模アプリでは分けすぎるとコードが冗長になる
・適用箇所を見極めることが重要(例:複雑な画面、再利用性が高いUI)
あとがき
ReactやVee特有の、HTMLコンポーネントを、無理やりVanillaJSで書き直して見ましたが、コードのボリュームや見た目のわかりやすさを比べると、
フレームワークの簡単さが手に取るようにわかりますね。
でも、個人的には、こうしたコンポーネントは、プログラムソースに埋め込むのではなく、コンポーネントファイルとしてフォルダ管理されている方が、その後の拡張などが効率的になるような気がします。
ちなみに、PHPなどでやる場合は、普通にこ右下構成は、プログラム内(HTML組み込みコード)でできてしまうので、Javascriptの悩みでもある、コンポーネント組み込みの難しさ(そもそもネイティブ機能で持っていないので)を際理解できるデザインパターンだと思いました。
0 件のコメント:
コメントを投稿