概要
本記事では、動的にUIが変化するフロントエンドアプリケーション(FEアプリ) の基本構造を
Vanilla JavaScript(命令的構造) と React(宣言的構造) の両視点から比較・整理します。
具体的には、TODOアプリを例にして、
「UIの生成・イベント処理・状態管理」をどのように構成するかを段階的に理解できる構成となっています。
目次
基本構造
Vanilla JS(命令的構造)
1. import / 初期設定セクション
2. DOM参照キャッシュ / 定数セクション
3. 関数定義セクション
| ├─ 3.1 データアクセス関数セクション(Storage I/O)
| ├─ 3.2 データ操作(ビジネスロジック)関数セクション(Model更新系)
| ├─ 3.3 イベントハンドラー関数セクション
| └─ 3.4 UI更新関数セクション(副作用を内包)
| ├─ 3.4.1 要素生成・属性設定
| ├─ 3.4.2 イベント付与(click, inputなど)
| ├─ 3.4.3 構造組み立て(appendChildでネスト構築)
| └─ 3.4.4 DOM挿入(append/removeで画面反映)
4. 初期化 / イベントハンドラー登録セクション
Vanilla JSでは、手続き的(命令的) にDOMを直接操作します。
createElement()、appendChild()、addEventListener()などを組み合わせ、
UIを構築・更新する流れを「副作用として明示的に」記述します。
React版(宣言的構造)
1. importセクション
2. 型定義セクション(TypeScript使用時)
3. 関数定義セクション
├─ 3.1 内部状態管理セクション
├─ 3.2 イベントハンドラーセクション
├─ 3.3 副作用処理セクション
└─ 3.4 返り値構築・ロジックセクション
Reactでは、宣言的(Declarative) にUIを記述します。
DOMを直接触る代わりに、useStateで状態を保持し、
状態変化に応じてReactが仮想DOMを再構築します。この構造により、UIの生成・更新・削除をReactが自動で最適化します。
コード例
Vanilla JS(命令的構造)
// ==============================
// 1. import / 初期設定セクション
// ==============================
import "./styles.css";
// ==============================
// 2. DOM参照キャッシュ / 定数セクション
// ==============================
// ※今回は必要最低限のため未使用(関数内で直接参照)
// const inputEl = document.getElementById("add-text");
// const addBtn = document.getElementById("add-button");
// const incList = document.getElementById("incomplete-list");
// const compList = document.getElementById("complete-list");
// ==============================
// 3. 関数定義セクション
// ==============================
// ------------------------------
// 3.1 データアクセス関数セクション(Storage I/O)
// ------------------------------
// ------------------------------
// 3.2 データ操作(ビジネスロジック)関数セクション(Model更新系)
// ------------------------------
// ------------------------------
// 3.3 イベントハンドラー関数セクション
// ------------------------------
const onClickAdd = () => {
// テキストボックスの値を取得し、初期化する(副作用)
const inputText = document.getElementById("add-text").value;
document.getElementById("add-text").value = "";
// 未完了リストに追加(UI更新関数呼び出し)
createIncompleteTodo(inputText);
};
// ------------------------------
// 3.4 UI更新関数セクション(副作用を内包)
// ------------------------------
const createIncompleteTodo = (todo) => {
// ------------------------------
// 3.4.1 要素生成・属性設定
// ------------------------------
const li = document.createElement("li");
const div = document.createElement("div");
div.className = "list-row";
const p = document.createElement("p");
p.className = "todo-item";
p.innerText = todo;
const completeButton = document.createElement("button");
completeButton.innerText = "完了";
const deleteButton = document.createElement("button");
deleteButton.innerText = "削除";
// ------------------------------
// 3.4.2 イベント付与(click, inputなど)
// ------------------------------
completeButton.addEventListener("click", () => {
const moveTarget = completeButton.closest("li");
completeButton.nextElementSibling.remove();
completeButton.remove();
const backButton = document.createElement("button");
backButton.innerText = "戻す";
backButton.addEventListener("click", () => {
const todoText = backButton.previousElementSibling.innerText;
createIncompleteTodo(todoText);
backButton.closest("li").remove();
});
moveTarget.firstElementChild.appendChild(backButton);
document.getElementById("complete-list").appendChild(moveTarget);
});
deleteButton.addEventListener("click", () => {
const deleteTarget = deleteButton.closest("li");
document.getElementById("incomplete-list").removeChild(deleteTarget);
});
// ------------------------------
// 3.4.3 構造組み立て(appendChildでネスト構築)
// ------------------------------
div.appendChild(p);
div.appendChild(completeButton);
div.appendChild(deleteButton);
li.appendChild(div);
// ------------------------------
// 3.4.4 DOM挿入(append/removeで画面反映)
// ------------------------------
document.getElementById("incomplete-list").appendChild(li);
};
// ==============================
// 4. 初期化 / イベントハンドラー登録セクション
// ==============================
document
.getElementById("add-button")
.addEventListener("click", onClickAdd);
React版(宣言的構造)
// ==============================
// 1. importセクション
// ==============================
import { useState } from "react";
import "./styles.css";
// ==============================
// 2. 型定義セクション(TypeScript使用時)
// ==============================
// ※今回は使用なし
// type Todo = string;
// type Props = { title: string };
// ==============================
// 3. 関数定義セクション(Appコンポーネント定義)
// ==============================
export default function App() {
// ------------------------------
// 3.1 内部状態管理セクション(useState)
// ------------------------------
const [inputText, setInputText] = useState("");
const [incompleteTodos, setIncompleteTodos] = useState<string[]>([]);
const [completeTodos, setCompleteTodos] = useState<string[]>([]);
// ------------------------------
// 3.2 イベントハンドラーセクション
// ------------------------------
const onChangeText = (e: React.ChangeEvent<HTMLInputElement>) => {
setInputText(e.target.value);
};
const onClickAdd = () => {
if (inputText === "") return;
setIncompleteTodos([...incompleteTodos, inputText]);
setInputText("");
};
const onClickComplete = (index: number) => {
const newIncomplete = [...incompleteTodos];
const doneTodo = newIncomplete.splice(index, 1)[0];
setIncompleteTodos(newIncomplete);
setCompleteTodos([...completeTodos, doneTodo]);
};
const onClickDelete = (index: number) => {
const newIncomplete = [...incompleteTodos];
newIncomplete.splice(index, 1);
setIncompleteTodos(newIncomplete);
};
const onClickBack = (index: number) => {
const newComplete = [...completeTodos];
const backTodo = newComplete.splice(index, 1)[0];
setCompleteTodos(newComplete);
setIncompleteTodos([...incompleteTodos, backTodo]);
};
// ------------------------------
// 3.3 副作用処理セクション(useEffectなど)
// ------------------------------
// ※今回は外部通信やライフサイクル処理がないため未使用
// ------------------------------
// 3.4 返り値構築・ロジックセクション(JSX構築)
// ------------------------------
return (
<div className="App">
<div className="input-area">
<input
id="add-text"
placeholder="TODOを入力"
value={inputText}
onChange={onChangeText}
/>
<button id="add-button" onClick={onClickAdd}>
追加
</button>
</div>
<div className="incomplete-area">
<h2>未完了のTODO</h2>
<ul id="incomplete-list">
{incompleteTodos.map((todo, index) => (
<li key={index}>
<div className="list-row">
<p className="todo-item">{todo}</p>
<button onClick={() => onClickComplete(index)}>完了</button>
<button onClick={() => onClickDelete(index)}>削除</button>
</div>
</li>
))}
</ul>
</div>
<div className="complete-area">
<h2>完了したTODO</h2>
<ul id="complete-list">
{completeTodos.map((todo, index) => (
<li key={index}>
<div className="list-row">
<p className="todo-item">{todo}</p>
<button onClick={() => onClickBack(index)}>戻す</button>
</div>
</li>
))}
</ul>
</div>
</div>
);
}
命令的構造 vs 宣言的構造
| 観点 | Vanilla JS(命令的) | React(宣言的) |
|---|---|---|
| DOM操作 |
createElement, appendChild など明示的 |
Reactが仮想DOMを自動制御 |
| 状態管理 | DOM・変数の直接操作 |
useState で一元管理 |
| 再描画 | 手動でDOM更新 | 状態変化時に自動再描画 |
| コード量 | 手続き的で冗長 | 構造的で整理されやすい |
| 学習目的 | DOM構造・イベント理解に最適 | 状態管理・UI設計の習得に最適 |
まとめ
- Vanilla JSでは、UIを「どう作るか」(手続き)を記述する。
- Reactでは、UIが「どうあるべきか」(宣言)を記述する。
- どちらもUIを動的に変化させるためのアプローチだが、
Reactは副作用を抽象化し、より明確に「状態駆動型UI」を実現する。