この記事は「構造パスで作るシンプルなフロントエンドフレームワーク(Structive)」Advent Calendarの4日目です。
Structiveについて詳しくはこちらより
前回のおさらい
3日目では、「UIと状態は同じデータの異なる表出」という核心的な思想を学びました。今日は、この思想がどのようにボイラープレートの排除につながるのか、技術的な観点から解説します。
ボイラープレートとは?
ボイラープレートとは、本質的なロジックではないが、フレームワークを使うために書かざるを得ない「定型的なコード」のことです。
典型的なボイラープレートの例
Reactで簡単なカウンターを作る場合:
import { useState } from 'react';
function Counter() {
// ボイラープレート1: useState の呼び出し
const [count, setCount] = useState(0);
// ボイラープレート2: イベントハンドラの定義
const increment = () => {
setCount(count + 1);
};
const decrement = () => {
setCount(count - 1);
};
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
);
}
やりたいこと: カウンターの値を表示して増減させる
書かされるコード:
-
useStateのインポートと呼び出し - setter関数(
setCount) - 2つのイベントハンドラ関数
実際のロジックは「数値を±1する」だけなのに、多くの定型コードが必要です。
構造パスによる解決
同じカウンターを構造パスで書くと:
<template>
<div>
<p>Count: {{ count }}</p>
<button data-bind="onclick:increment">+</button>
<button data-bind="onclick:decrement">-</button>
</div>
</template>
<script type="module">
export default class {
count = 0;
increment() {
this.count += 1;
}
decrement() {
this.count -= 1;
}
}
</script>
不要になったもの:
-
useStateの呼び出し - setter関数(
setCount) - ラッパー関数
なぜこれが可能なのか? それを順を追って見ていきましょう。
1. 宣言的なUIの記述
従来の命令的なアプローチ
従来、JavaScriptでUIを更新するには、DOMを直接操作する必要がありました:
// 命令的:「どうやって」変更するかを記述
const countElement = document.querySelector('#count');
countElement.textContent = count;
const button = document.querySelector('#increment-btn');
button.addEventListener('click', () => {
count += 1;
countElement.textContent = count; // 手動で更新
});
問題点:
- DOMの取得と操作が煩雑
- 状態とUIの同期を手動で管理
- コードが冗長で読みにくい
宣言的なアプローチ
構造パスでは、UIが「どう見えるべきか」だけを記述します:
<p>Count: {{ count }}</p>
ポイント:
- 「
countという値を表示する」と宣言するだけ - 「どうやって更新するか」は書かない
- フレームワークが自動的に更新を管理
2. 状態の直接操作
なぜsetter関数が不要なのか?
従来のフレームワークでsetter関数が必要な理由:
const [count, setCount] = useState(0);
// これは動かない
count = 5; // ❌ 直接変更してもUIは更新されない
// setter関数を使う必要がある
setCount(5); // ✅ フレームワークに「更新があった」と通知
setter関数は、フレームワークに「状態が変わった」ことを伝えるための仕組みです。
構造パスのアプローチ
構造パスでは、状態を直接変更するだけでOKです:
this.count = 5; // ✅ これだけでUIが更新される
なぜ可能なのか?
フレームワークは、状態オブジェクトをProxyでラップしています。Proxyは、プロパティの変更を自動的に検知できます(詳細は5日目で解説)。
// フレームワーク内部(概念的なイメージ)
const state = new Proxy(yourStateObject, {
set(target, property, value) {
target[property] = value;
// 自動的にUIを更新
updateUI(property);
return true;
}
});
開発者は何も意識せず、純粋なJavaScriptとして状態を操作できます。
3. 双方向バインディングの自動化
従来のフォーム処理
入力フォームを扱う場合、従来は大量のコードが必要でした:
function Form() {
const [name, setName] = useState("");
const [email, setEmail] = useState("");
// ボイラープレート:各フィールドごとにハンドラ
const handleNameChange = (e) => {
setName(e.target.value);
};
const handleEmailChange = (e) => {
setEmail(e.target.value);
};
return (
<form>
<input
value={name}
onChange={handleNameChange}
/>
<input
value={email}
onChange={handleEmailChange}
/>
</form>
);
}
フィールドが10個あれば、10個のハンドラが必要です。
構造パスの自動バインディング
<template>
<form>
<input data-bind="value: user.name">
<input data-bind="value: user.email">
</form>
</template>
<script type="module">
export default class {
user = {
name: "",
email: ""
};
}
</script>
イベントハンドラは不要です。
data-bind="value: user.name"と書くだけで:
- 状態の値が
inputに反映される(状態 → UI) - ユーザーの入力が状態に反映される(UI → 状態)
フレームワークが自動的に双方向のバインディングを設定します。
4. イベントハンドラの簡潔な記述
従来のイベント処理
function ProductList({ products }) {
const handleDelete = (productId) => {
// 削除処理
};
return (
<ul>
{products.map(product => (
<li key={product.id}>
{product.name}
{/* ラッパー関数が必要 */}
<button onClick={() => handleDelete(product.id)}>
Delete
</button>
</li>
))}
</ul>
);
}
各アイテムにイベントハンドラをバインドするために、アロー関数でラップする必要があります。
構造パスのイベント処理
<template>
<ul>
{{ for:products }}
<li>
{{ products.*.name }}
<button data-bind="onclick:deleteProduct">Delete</button>
</li>
{{ endfor: }}
</ul>
</template>
<script type="module">
export default class {
products = [...];
deleteProduct(event, index) {
// index は自動的に渡される
this.products = this.products.toSpliced(index, 1);
}
}
</script>
ラッパー関数は不要です。
フレームワークがループコンテキストを自動的に追跡し、クリックされたアイテムのインデックスをdeleteProductメソッドに渡します。
ボイラープレート排除の実例比較
実際のアプリケーションで、どれだけコードが削減されるか見てみましょう。
例:商品カートの数量変更
React(24行):
function CartItem({ item, onUpdateQuantity }) {
const [quantity, setQuantity] = useState(item.quantity);
const handleChange = (e) => {
const newQuantity = parseInt(e.target.value);
setQuantity(newQuantity);
};
const handleBlur = () => {
onUpdateQuantity(item.id, quantity);
};
return (
<div>
<span>{item.name}</span>
<input
type="number"
value={quantity}
onChange={handleChange}
onBlur={handleBlur}
/>
</div>
);
}
構造パス(17行):
<template>
{{ for:cart.items }}
<div>
<span>{{ cart.items.*.name }}</span>
<input
type="number"
data-bind="valueAsNumber: cart.items.*.quantity"
>
</div>
{{ endfor: }}
</template>
<script type="module">
export default class {
cart = { items: [...] };
}
</script>
約3分の2のコード量で同じ機能を実現。
なぜボイラープレートが不要になるのか?
まとめると、以下の3つの仕組みが連携しています:
1. 構造パスによる統一的なアクセス
UIと状態が同じ構造パスを共有することで、明示的なマッピングが不要。
2. Proxyによる自動検知
状態の変更を自動的に検知し、UIを更新。setter関数が不要。
3. フレームワークの賢い推論
-
value属性 → 双方向バインディング -
onclick属性 → ループインデックスの自動渡し -
forブロック → ループコンテキストの自動管理
開発者が書くコードは「本質的なロジック」だけです。
開発者体験の向上
ボイラープレートの排除は、単なる「コード量の削減」以上の意味があります:
1. 認知負荷の低減
考えることが減ります:
- setter関数の名前を考えなくていい
- イベントハンドラをどう書くか悩まなくていい
- 状態の更新をどう伝播させるか考えなくていい
2. バグの削減
書くコードが少ない = バグが入る場所が少ない:
- setter関数の呼び忘れがない
- イベントハンドラのバインディングミスがない
- 状態の不整合が起きにくい
3. 学習コストの低減
覚えることが少ない:
-
useState、useEffectなどのAPIを覚える必要がない - ライフサイクルメソッドを理解する必要がない
- 純粋なJavaScriptの知識だけでOK
まとめ
今日は、構造パスがどのようにボイラープレートを排除するか学びました:
排除される主なボイラープレート:
- useState の呼び出し
- setter関数
- イベントハンドラのラッパー関数
- 手動のDOM操作
実現する仕組み:
- 宣言的なUI記述
- 状態の直接操作(Proxy)
- 自動的な双方向バインディング
- フレームワークの賢い推論
開発者への利点:
- コード量の削減(約1/3)
- 認知負荷の低減
- バグの削減
- 学習コストの低減
次回予告:
明日は、「仮想DOMなしでリアクティビティを実現する方法」を解説します。構造パスとProxyを使って、どのように効率的なUI更新を実現するのか、技術的な詳細に踏み込みます。
次回: Day 5「仮想DOMなしでリアクティビティを実現する方法」
ボイラープレート排除について質問があれば、コメントでぜひ!