宣言的 UI と命令的 UI — 状態から UI を導く
React も Vue も Svelte も「宣言的 UI」と説明されます。一方で jQuery で書かれたコードは「命令的」と呼ばれます。この違いは書き方の流行ではなく、状態と表示のずれをどう防ぐかという設計の分かれ目です。
この章で学ぶこと
- 命令的 UI と宣言的 UI が、それぞれ何を記述しているか
- 宣言的 UI で状態と表示の不整合が減る理由
- 宣言的 UI でも DOM を直接触る必要が残る場面
- 宣言的 UI が払っている代償
JavaScript で DOM を操作した経験があると比較が実感できます。React などのフレームワークの経験は不要です。
同じ画面を 2 通りで書く
「読み込み中はスピナー、失敗ならエラー文、成功なら一覧」を表示する画面を考えます。
命令的に書く
// 命令的 ー DOM を変える手順を並べる
function showLoading() {
document.querySelector('#spinner').style.display = 'block';
document.querySelector('#error').style.display = 'none';
document.querySelector('#list').style.display = 'none';
}
function showError(message) {
document.querySelector('#spinner').style.display = 'none';
document.querySelector('#error').style.display = 'block';
document.querySelector('#error').textContent = message;
document.querySelector('#list').style.display = 'none';
}
function showList(items) {
document.querySelector('#spinner').style.display = 'none';
document.querySelector('#error').style.display = 'none';
const list = document.querySelector('#list');
list.style.display = 'block';
list.innerHTML = items.map((i) => `<li>${i.name}</li>`).join('');
}
書いているのは DOM を変える手順です。3 つの関数それぞれが、3 つの要素すべての表示状態を面倒みています。
宣言的に書く
// 宣言的 ー 状態から画面への対応を書く
type Status =
| {kind: 'loading'}
| {kind: 'error'; message: string}
| {kind: 'success'; items: Item[]};
function ItemList({status}: {status: Status}) {
if (status.kind === 'loading') return <Spinner />;
if (status.kind === 'error') return <ErrorText>{status.message}</ErrorText>;
return (
<ul>
{status.items.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}
書いているのは 状態から画面への対応です。DOM をどう書き換えるかは一行も出てきません。状態が変われば、フレームワークが前回の結果と比べて必要な差分だけを DOM に反映します。
記述しているものが違う
| 命令的 UI | 宣言的 UI | |
|---|---|---|
| 書くこと | DOM を変える手順 | 状態から画面への対応 |
| DOM 操作 | 開発者が手順を書く | フレームワークが差分から導く |
| 代表例 | jQuery、素の DOM API | React、Vue、Svelte |
| 状態の置き場 | DOM そのもの、または変数と DOM の両方 | 状態変数 (単一の真実の源) |
命令的 UI では「今どういう表示になっているか」が DOM の中に散らばります。宣言的 UI では状態変数だけが真実で、画面はそこから毎回導かれます。
なぜ不整合が減るのか
命令的 UI が壊れやすいのは、書かなければいけない遷移の数が状態の数より速く増えるからです。
状態が 3 つなら、そのあいだの遷移は 3 × 2 = 6 通りあります。4 つなら 12 通り、5 つなら 20 通りです。命令的に書くと、この遷移のそれぞれで「何を消して何を出すか」を漏れなく書く必要があります。
先ほどの showError を見てください。スピナーを消し、エラーを出し、一覧を消す。3 つすべてに触れています。ここで 4 つ目の要素 (たとえば「再試行ボタン」) を足すと、既存の 3 つの関数すべてに追記が要ります。1 つでも忘れると、エラー表示なのに再試行ボタンが消えたまま、といったずれが残ります。
宣言的 UI では、書くのは状態の数だけです。遷移そのものを記述しません。再試行ボタンを足すときも、それを出したい分岐に 1 行足すだけで済みます。他の分岐は関数が最初から「その状態のときの完成形」を返しているので、意識せずとも正しくなります。
これが「状態と UI の不整合が起きにくい」と言われる理由です。不整合が減るのは宣言的 UI のメリットであって、デメリットではありません。
宣言的 UI でも DOM を直接触る場面はある
「宣言的 UI なら DOM 操作は一切不要」ではありません。状態から導けない操作が残ります。
- 入力欄にフォーカスを当てる
- 特定の位置までスクロールする
- 動画や音声の再生と停止
canvasへの描画- 要素の実寸を測る
これらは「画面がどう見えるか」ではなく「今この瞬間に何をするか」なので、状態の写像として表せません。React ではこうした操作のために ref という逃げ道が用意されています。
つまり宣言的 UI は、DOM 操作を禁止する仕組みではなく、大部分を状態からの導出に置き換えたうえで、残りを明示的な例外として隔離する仕組みです。
代償
宣言的 UI はただで手に入るものではありません。
| 代償 | 内容 |
|---|---|
| ランタイムの重さ | 差分計算の仕組みをブラウザに送り込む必要がある |
| 差分計算のコスト | 状態が変わるたびに前回との比較が走る |
| 制御の間接性 | 「なぜこの DOM がこうなったか」がフレームワークの中に隠れる |
| 学習コスト | 状態の設計、再レンダリングの条件、副作用の扱いを覚える必要がある |
小さなランディングページに 1 か所だけ動きを付けるなら、素の DOM API のほうが軽くて速い場合があります。宣言的 UI が効くのは、状態の種類が多く、それが画面の複数箇所に影響するアプリケーションです。実際にどう状態を設計するかは React ガイド — State が扱っています。
よくある誤解
「宣言的 UI は状態と UI の不整合が起きやすい」 — 逆です。不整合を減らすことが宣言的 UI の主な動機です。画面を状態から毎回導出するので、遷移の書き漏らしという失敗の形そのものが無くなります。
「宣言的 UI では開発者が DOM の操作手順を細かく制御する」 — それは命令的 UI の説明です。宣言的 UI では手順を書かず、状態と表示の対応だけを書きます。
「宣言的 UI なら DOM に触れることは一切ない」 — フォーカス、スクロール、メディア再生などは状態から導けないので、ref のような明示的な経路で直接操作します。
「宣言的に書けば速くなる」 — 速くなるとは限りません。差分計算のぶんだけ実行時のコストは増えます。得られるのは実行速度ではなく、状態が増えたときの正しさと保守性です。
確認問題
問 1. 次のコードは宣言的と命令的のどちらですか。理由も答えてください。
const badge = document.querySelector('#cart-badge');
badge.textContent = String(count);
badge.classList.toggle('hidden', count === 0);
答え: 命令的
要素を取得し、テキストを書き換え、クラスを付け外しする、という操作の手順を書いているためです。「カート件数が 0 なら非表示、そうでなければ件数を表示する」という対応関係は、コードの形からは直接読み取れず、3 行を追って組み立てる必要があります。
同じことを宣言的に書くと次のようになります。対応関係が式の形でそのまま現れます。
function CartBadge({count}: {count: number}) {
if (count === 0) return null;
return <span className="badge">{count}</span>;
}
問 2. 命令的 UI で状態が 4 つに増えたとき、書かなければならない遷移は何通りですか。
答え: 12 通り (4 × 3)
ある状態から別の状態へ移る組み合わせなので、4 つの状態それぞれから残り 3 つへの遷移で 12 通りです。状態が 5 つなら 20 通りになります。
宣言的 UI で書くのは状態の数だけ (4 つなら 4 分岐) です。遷移の記述が不要になるので、状態が増えたときの増え方が緩やかになります。この差が保守性の差になります。
問 3. 宣言的 UI を使っているプロジェクトで「送信ボタンを押したら入力欄にフォーカスを戻す」を実装したいです。状態として持つべきでしょうか。
答え: 状態として持たず、ref で直接操作します。
フォーカスは「その瞬間に一度だけ行う操作」で、「今の状態ならこう見える」という対応では表せません。仮に shouldFocus のような状態を持たせても、フォーカスを当てたあとにその状態を戻す処理が必要になり、かえって命令的な手続きが増えます。
宣言的 UI は DOM 操作を禁止しません。状態から導ける部分を導出に任せ、導けない部分を ref という明示的な例外として切り出す、という分担です。
まとめ
- 命令的 UI は現在の画面をどう変えるかという手順を書き、宣言的 UI はこの状態ならこう見えるという結果を書きます
- React や Vue は宣言的 UI、jQuery は命令的 UI の代表例です
- 命令的 UI では書くべき遷移が状態の数より速く増えます。宣言的 UI で書くのは状態の数だけです
- 宣言的 UI の主な動機は状態と表示の不整合を減らすことです。不整合が増えるわけではありません
- フォーカス・スクロール・メディア再生など状態から導けない操作は、宣言的 UI でも直接 DOM を触ります
- 代償はランタイムの重さ、差分計算のコスト、制御の間接性です
- React ガイド — この本について — React が宣言的 UI をどう実現しているか
- React ガイド — State (useState) — 状態を単一の真実の源として設計する方法
- MDN — DOM の紹介
次に読む
- レンダリングパターン — その UI を「どこで」組み立てるかの選択