この記事は「構造パスで作るシンプルなフロントエンドフレームワーク (Structive)」Advent Calendarの3日目です。
Structiveについて詳しくはこちらより
前回のおさらい
2日目では、構造パスの基本的な仕組みを学びました:
- 構造パスはデータへの「住所」
- ドット記法でネストを表現
- ワイルドカード(
*)で配列を抽象化
今日は、このフレームワークの核心となる思想「UIと状態は同じデータの異なる表出」について深く掘り下げます。
従来のアプローチの問題点
まず、従来のフレームワークでUIと状態がどう扱われているか見てみましょう。
Reactの例
// 状態の定義
const [user, setUser] = useState({
profile: {
name: "Alice",
email: "alice@example.com"
}
});
// UIの定義
return (
<div>
<h2>{user.profile.name}</h2>
<p>{user.profile.email}</p>
</div>
);
// 状態の更新
setUser({
...user,
profile: {
...user.profile,
name: "Bob"
}
});
問題点:
- 状態とUIが異なる形式 - JSXとJavaScriptオブジェクトは別物
-
更新に中間層 -
setUserという関数を経由する必要がある - 不変性の維持 - スプレッド構文で元のオブジェクトをコピー
つまり、状態とUIは「別々のもの」として扱われています。
新しい思想:UIと状態は同じもの
このフレームワークでは、まったく異なる考え方をします。
UIと状態は、同じデータの異なる表出である
これは、以下を意味します:
- UIは状態を「見る窓」に過ぎない
- 状態が変わればUIも変わる
- 両者は同じ構造を共有する
具体例で理解する
// 状態の定義
class State {
user = {
profile: {
name: "Alice",
email: "alice@example.com"
}
};
}
<!-- UIの定義 -->
<div>
<h2>{{ user.profile.name }}</h2>
<p>{{ user.profile.email }}</p>
</div>
注目してください:
- UIの
{{ user.profile.name }} - 状態の
user.profile.name
まったく同じ構造パスを使っています。
状態の更新もシンプル
// 状態の更新
this["user.profile.name"] = "Bob";
setter関数も、スプレッド構文も不要です。純粋なJavaScriptオブジェクトとして構造パスで直接変更するだけです。
単一の真実の源(Single Source of Truth)
この思想の最大のメリットは、データが一箇所にしか存在せず、そのデータの位置は構造パスにより一意に特定されることです。
UIでも状態でも、同じ構造パスは同じデータを参照すること保証します。
可読性と保守性の向上
UIの依存関係が一目瞭然
構造パスを使うと、UIがどのデータに依存しているかが明確です:
<div class="product-card">
<h3>{{ product.name }}</h3>
<p>{{ product.description }}</p>
<span class="price">${{ product.price }}</span>
<button data-bind="onclick:addToCart">Add to Cart</button>
</div>
このUIを見れば、以下がすぐにわかります:
-
product.nameに依存 -
product.descriptionに依存 -
product.priceに依存 -
addToCartメソッドを呼び出す
影響範囲の追跡が簡単
状態を変更するとき、どのUIが影響を受けるか簡単にわかります:
// product.price を変更する
this["product.price"] = 1999;
// 影響を受けるUI: {{ product.price }} を使っている箇所
// エディタで「product.price」を検索するだけ!
従来のフレームワークでは、propsの連鎖やコンテキストを追う必要がありますが、構造パスなら単純な文字列検索で完結します。
具体例:ショッピングカート
実際のアプリケーションで、この思想がどう機能するか見てみましょう。
状態の定義
class State {
cart = {
items: [
{ productId: 1, quantity: 2 },
{ productId: 5, quantity: 1 }
]
};
products = [
{ id: 1, name: "Laptop", price: 999 },
{ id: 2, name: "Mouse", price: 29 },
// ...
];
}
UIテンプレート
<table>
<thead>
<tr>
<th>Product</th>
<th>Quantity</th>
<th>Price</th>
</tr>
</thead>
<tbody>
{{ for:cart.items }}
<tr>
<td>{{ cart.items.*.product.name }}</td>
<td>
<input type="number" data-bind="valueAsNumber: cart.items.*.quantity">
</td>
<td>${{ cart.items.*.price }}</td>
</tr>
{{ endfor: }}
</tbody>
</table>
何が起きているか?
-
UIは状態を描画しているだけ
-
{{ cart.items.*.product.name }}→ 商品名を表示 -
{{ cart.items.*.quantity }}→ 数量を表示 -
{{ cart.items.*.price }}→ 価格を表示
-
-
状態とUIは同じ構造
- UIの構造パスと状態のプロパティが完全に一致
- 中間的な変換やマッピングが不要
-
双方向バインディングも自然
<input data-bind="valueAsNumber: cart.items.*.quantity">- ユーザーが入力 → 状態が更新 → UIが更新
スケーラビリティへの影響
この思想は、大規模アプリケーションでも威力を発揮します。
機能追加が簡単
新しい機能を追加するとき:
// 1. 状態にプロパティを追加
class State {
cart = {
items: [...],
discount: 0, // 新しい機能:割引
couponCode: ""
};
}
<!-- 2. UIに対応する要素を追加 -->
<div>
<input data-bind="value: cart.couponCode" placeholder="Coupon Code">
<p>Discount: {{ cart.discount }}%</p>
</div>
それだけです。 setter関数の定義も、イベントハンドラの追加も不要です。
チーム開発での共通言語
構造パスは、チームメンバー間の共通言語になります:
デザイナー: 「この商品名の部分、フォントを変えたいです」
開発者: 「{{ product.name }}の部分ですね。CSSで.product-nameクラスを追加します」
バックエンド: 「APIのレスポンスにuser.isVerifiedを追加しました」
フロントエンド: 「了解。{{ user.isVerified }}でUIに表示します」
構造パスという明確な「住所」があるため、コミュニケーションがスムーズになります。
3つの原則
この思想をまとめると、以下の3つの原則になります:
1. 同じ構造の原則
UIと状態は同じデータ構造を持つ。両者は同じ構造パスでアクセス可能。
2. 単一の真実の原則
状態は一箇所にのみ存在する(Single Source of Truth)。UIは常にその状態を反映する。
3. 直接操作の原則
状態を変更するのに中間層は不要。純粋なJavaScriptオブジェクトとして直接操作する。
まとめ
今日は、このフレームワークの核心的な思想を学びました:
UIと状態は同じデータの異なる表出:
- UIは状態を「見る窓」
- 両者は同じ構造パスを共有
- データは常に一箇所にのみ存在
この思想がもたらす利点:
- 可読性の向上(依存関係が明確)
- 保守性の向上(影響範囲が追跡しやすい)
- スケーラビリティ(機能追加が簡単)
- チーム開発の効率化(共通言語)
次回予告:
明日は、「ボイラープレートを排除する仕組み」について解説します。setter関数やイベントハンドラが不要になる理由を、技術的な観点から掘り下げます。
次回: Day 4「ボイラープレートを排除する仕組み」
この思想について、疑問や意見があればコメントで教えてください!