
Flux/Reduxは、Facebookが提唱したアーキテクチャパターンです。
MVVMの派生で、Reactの基礎になっています。
この2つの違いも含めて解説してみたいと思います。
Fluxとは
MVCのような双方向データバインディングではなく、データの一方向フロー(unidirectional data flow)を重視しているパターンになっています。
アプリの状態管理をシンプルで予測可能にすることを目的にして、データの流れを一方向にすることで、デバッグしやすくするのが特徴です。
構成
[View] → [Action] → [Dispatcher] → [Store] → [View]
View
UI部分(Reactコンポーネントなど)
ユーザー操作によって Action を発行する
Action
何が起きたかを表す「イベントオブジェクト」
例: { type: "ADD_TODO", text: "牛乳を買う" }
Dispatcher
すべてのActionを一元管理する「ハブ」
各Storeに対してActionを配信する
Store
アプリの状態(State)を保持
DispatcherからActionを受け取り、状態を更新
状態が変わるとViewに通知
データの流れ(Fluxサイクル)
1. ユーザー操作 → ViewがActionを発行
↓
2. Dispatcher がActionを各Storeへ配信
↓
3. Store が状態を更新
↓
4. Store → View に変更通知
↓
5. View が再描画
サンプルコード
// === Dispatcher ===
class Dispatcher {
constructor() {
this.listeners = [];
}
register(fn) {
this.listeners.push(fn);
}
dispatch(action) {
this.listeners.forEach(fn => fn(action));
}
}
// === Action ===
const Actions = {
increment(dispatcher) {
dispatcher.dispatch({ type: 'INCREMENT' });
}
};
// === Store ===
class CounterStore {
constructor(dispatcher) {
this.count = 0;
this.listeners = [];
dispatcher.register((action) => {
switch (action.type) {
case 'INCREMENT':
this.count++;
this.emitChange();
break;
}
});
}
getState() {
return this.count;
}
subscribe(listener) {
this.listeners.push(listener);
}
emitChange() {
this.listeners.forEach(fn => fn());
}
}
// === View ===
const dispatcher = new Dispatcher();
const store = new CounterStore(dispatcher);
store.subscribe(() => {
document.getElementById('count').textContent = store.getState();
});
document.body.innerHTML = `
<h2>Flux Counter</h2>
<div>Count: <span id="count">0</span></div>
<button id="btn">+1</button>
`;
document.getElementById('btn').addEventListener('click', () => {
Actions.increment(dispatcher);
});
Reduxとは
Fluxをベースにした、よりシンプルで汎用的な状態管理ライブラリです。
Reactでよく使われますが、React専用ではありません。
Reduxの3原則
1. 単一のストア
アプリ全体の状態は1つのオブジェクトツリーで管理
例:
{
todos: [...],
user: {...}
}
2. 状態は読み取り専用
直接変更せず、Actionを介してのみ更新
3. 変更は純粋関数(Reducer)で行う
StateとActionを受け取り、新しいStateを返す
副作用を持たない
構成
[View] → [Action] → [Reducer] → [Store] → [View]
Action
状態変更の意図を表すオブジェクト
Reducer
現在のStateとActionから新しいStateを計算する純粋関数
Store
状態(State)を保持
ActionをdispatchしてReducerに渡す
View
状態を購読し、変更に応じて再レンダリング
Fluxとの違い
| 項目 |
Flux |
Redux |
| Store数 |
複数可 |
単一 |
| 状態管理 |
Storeが内部で管理 |
ReducerがStateを生成 |
| Dispatcher |
あり |
不要(dispatch関数で代替) |
| 状態変更 |
Store内部で更新 |
不変のStateを新しく生成 |
| 概念 |
設計思想(アーキテクチャ) |
実装ライブラリ |
サンプルコード
// === Reducer ===
function counter(state = { count: 0 }, action) {
switch (action.type) {
case 'INCREMENT':
return { count: state.count + 1 };
default:
return state;
}
}
// === Store ===
const store = Redux.createStore(counter);
// === View ===
document.body.innerHTML = `
<h2>Redux Counter</h2>
<div>Count: <span id="count">0</span></div>
<button id="btn">+1</button>
`;
// 状態が変わったらUI更新
store.subscribe(() => {
document.getElementById('count').textContent = store.getState().count;
});
// ボタン押下 → dispatch
document.getElementById('btn').addEventListener('click', () => {
store.dispatch({ type: 'INCREMENT' });
});
Canvas実装デモ
FluxとReduxのデータフロー構造の違いを、Canvasで可視化するデモです。
<canvas id="flowCanvas" width="800" height="400"></canvas>
<script>
const canvas = document.getElementById("flowCanvas");
const ctx = canvas.getContext("2d");
function drawBox(x, y, w, h, label, color) {
ctx.fillStyle = color;
ctx.strokeStyle = "#333";
ctx.lineWidth = 2;
ctx.fillRect(x, y, w, h);
ctx.strokeRect(x, y, w, h);
ctx.fillStyle = "#fff";
ctx.font = "16px sans-serif";
ctx.textAlign = "center";
ctx.fillText(label, x + w / 2, y + h / 2 + 5);
}
function drawArrow(x1, y1, x2, y2, text) {
const dx = x2 - x1, dy = y2 - y1;
const angle = Math.atan2(dy, dx);
const len = Math.sqrt(dx * dx + dy * dy);
ctx.save();
ctx.translate(x1, y1);
ctx.rotate(angle);
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(len - 10, 0);
ctx.strokeStyle = "#000";
ctx.lineWidth = 2;
ctx.stroke();
// 矢印先端
ctx.beginPath();
ctx.moveTo(len - 10, 0);
ctx.lineTo(len - 20, -5);
ctx.lineTo(len - 20, 5);
ctx.closePath();
ctx.fill();
// ラベル
ctx.fillStyle = "#000";
ctx.font = "12px sans-serif";
ctx.fillText(text, len / 2 - 15, -10);
ctx.restore();
}
// 描画クリア
ctx.clearRect(0, 0, canvas.width, canvas.height);
// === Flux側 ===
ctx.fillStyle = "#333";
ctx.font = "20px sans-serif";
ctx.fillText("🌀 Flux", 120, 40);
drawBox(50, 80, 100, 60, "View", "#3b82f6");
drawBox(200, 80, 120, 60, "Action", "#f97316");
drawBox(380, 80, 140, 60, "Dispatcher", "#22c55e");
drawBox(580, 80, 120, 60, "Store", "#a855f7");
drawArrow(150, 110, 200, 110, "dispatch()");
drawArrow(320, 110, 380, 110, "dispatch");
drawArrow(520, 110, 580, 110, "update()");
drawArrow(580, 140, 100, 140, "notify → View");
// === Redux側 ===
ctx.fillStyle = "#333";
ctx.font = "20px sans-serif";
ctx.fillText("⚡ Redux", 120, 250);
drawBox(50, 280, 100, 60, "View", "#3b82f6");
drawBox(200, 280, 120, 60, "Action", "#f97316");
drawBox(380, 280, 140, 60, "Reducer", "#22c55e");
drawBox(580, 280, 120, 60, "Store", "#a855f7");
drawArrow(150, 310, 200, 310, "dispatch()");
drawArrow(320, 310, 380, 310, "reduce()");
drawArrow(520, 310, 580, 310, "setState()");
drawArrow(580, 340, 100, 340, "subscribe → View");
</script>
表現しているポイント
| パターン |
特徴 |
| Flux |
Dispatcherを中心に「複数のStore」へ流す。一方向のイベントループ構造。 |
| Redux |
Dispatcherがなく、「1つのStore+Reducer」で管理。状態はImmutableで更新。 |
補足
Flux:データの流れが“イベント中心”で、複数のStoreが独立して存在できる。
Redux:状態の流れが“関数型思考”で、全状態をReducerが一括管理。
あとがき
MVVMをさらに使いやすくしたイメージのFlux/Reduxパターンでしたね。
Javascript特有のDOM各種のイベント設置とそれらを管理するのを効率的にするのがわかります。
実際にプログラミングをしていても、Webイベント管理は、よく迷子になりがちなので、こうしたデザインパターンを熟知して設計対応するのが良いですね。
0 件のコメント:
コメントを投稿