目次
1.はじめに
この記事は、note 記事のマガジン: DakotaRed 流:Vibe Coding への道 と連動しています。
今回は、独学『バイブコーディングをやってみよう#3』- バイブコーディングこそソース管理が重要では!で作成した ToDo アプリのコード解説記事です。
この解説記事では、ToDo アプリでの4つの重要な要素:「状態管理」「永続性」「ユーザー体験」「セキュリティ」を中心として解説しています。
この記事の内容を理解することで、以下の基礎が身につきます:
✅ LocalStorage を利用したデータ管理(永続化)
✅ イミュータブルな配列操作(状態管理)
✅ タスクリストの並び替えロジック(ユーザー体験)
✅ テーマ管理(ダークモード/ライトモードの切り替え)(ユーザー体験)
✅ Electron のセキュリティ設計(IPC/Preload)(セキュリティ)
2.アプリの全体構造
このアプリは大きく分けて 2 つの部分 から成り立っています:
1️⃣ メインプロセス(Electron 側)
- 役割: アプリのウィンドウを作成・管理する「司令塔」のような存在
-
場所:
electron/フォルダ内 - 使用言語: JavaScript (CommonJS 形式)
2️⃣ レンダラープロセス(React 側)
- 役割: 画面の見た目や操作を担当する「UI 担当」
-
場所:
src/フォルダ内 - 使用言語: JavaScript (ES Modules 形式) + JSX
NOTE:なぜ形式が混在しているの?
Electron のメインプロセスは Node.js 環境で動くため伝統的な CommonJS (.cjs) を使用し、レンダラープロセス(React)は現代的なブラウザ向けビルドツール(Vite)で制御されるため ES Modules (.jsx) を使用しています。この使い分けにより、デスクトップ機能の安定性と最新のフロントエンド開発体験を両立させています。
3.ファイルの構成と役割
My ToDo App/
├── electron/
│ ├── main.cjs ← アプリのウィンドウを作る
│ └── preload.cjs ← セキュリティのための橋渡し
├── src/
│ ├── main.jsx ← Reactアプリの起動ポイント
│ ├── App.jsx ← メインのToDoアプリ本体
│ ├── App.css ← App.jsxのスタイル
│ └── index.css ← 全体に適用されるスタイル
├── index.html ← アプリの土台となるHTML
├── package.json ← アプリの設定と依存関係
└── vite.config.js ← ビルドツールの設定
4.開発の核となる5つの重要ポイントのコード解説
アプリで扱うデータの構造
コード解説に入る前に、このアプリで扱う「タスク(Todo)」がどのような情報を持っているかを確認しておきましょう。
1. 💾 データの永続化:アプリを閉じてもタスクが消えない仕組み
ToDo アプリにとって最も重要なのは、入力したデータが失われないことです。このアプリでは、ウェブブラウザの標準機能である LocalStorage を利用してデータの永続化を実現しています。
実現方法:
起動時の読み込み: todos の状態を初期化する際、まず localStorage.getItem('todos') で保存されたデータを読み込みます。データがあればそれを使い、なければ空の配列 [] で開始します。
// src/App.jsx
const [todos, setTodos] = useState(() => {
const saved = localStorage.getItem('todos')
return saved ? JSON.parse(saved) : [] // データがあればJSONをパースして返す
})
変更時の自動保存: React の useEffect フック を使用し、タスク一覧 (todos) の値が変わるたびに自動で実行されるように設定します。
// src/App.jsx
useEffect(() => {
// todosが変更されたら、新しいリストを文字列化してLocalStorageに保存
localStorage.setItem("todos", JSON.stringify(todos));
}, [todos]); // [todos] が依存配列。この値が変わると上の処理が実行される
ポイント:
useEffect の依存配列 [todos] のおかげで、開発者は明示的に保存関数を呼ぶ必要がなく、「タスクの状態が最新になったら自動で保存される」という堅牢な仕組みにすることが可能です。
2. ✅ タスクリストの並び替えロジック:使いやすさの向上
ToDo リストは単に追加した順に並べるだけでは不便です。このアプリでは、実用的な優先順位に基づいて表示順を決定しています。
実現方法:
sortedTodos という変数で、元の todos をコピーしてから sort() メソッド を使って並び替えています。
// src/App.jsx (抜粋)
const sortedTodos = [...todos].sort((a, b) => {
// 1. 完了状態が異なる場合: 未完了 (a.completedがfalse) を上に表示
if (a.completed !== b.completed) return a.completed ? 1 : -1;
// 2. 未完了タスク同士の場合: 期日 (dueDate) が近い順に表示
if (!a.completed && a.dueDate && b.dueDate) {
return new Date(a.dueDate) - new Date(b.dueDate);
}
// ... その他の並び替えルール
// 3. 両方期日なしの場合: ID (作成時刻) が新しい順 (b.id - a.id)
return b.id - a.id;
});
ポイント:
ユーザーが最も気にするべきタスク(未完了かつ期日が近いもの)がリストの先頭に来るように、細かく条件を定義しています。
3. 🎨 テーマ管理と CSS 変数:柔軟で統一されたデザイン
モダンなデスクトップアプリとして、ダークモードとライトモードの切り替えをスムーズに行うための仕組みを導入しています。
実現方法:
React と DOM の連携: テーマ (theme) の状態が変わるたびに useEffect が実行され、HTML のルート要素 (document.documentElement) に data-theme="dark" や data-theme="light" のような属性を設定します。
// src/App.jsx
useEffect(() => {
document.documentElement.setAttribute("data-theme", theme); // HTMLタグに属性を設定
// ... LocalStorageに保存
}, [theme]);
CSS 変数の利用: src/index.css では、色の値を直接書くのではなく、--text-primary や --accent-color といった CSS 変数 として定義しています。
/* src/index.css (抜粋) */
:root {
/* 全てに適用されるデフォルト値 */
--text-primary: #1e293b;
}
/* data-theme="dark"が設定された時だけこの色が上書きされる */
[data-theme="dark"] {
--text-primary: #f8fafc; /* メインテキストの色を白っぽく変更 */
}
ポイント:
CSS 変数を使うことで、JavaScript 側で属性を切り替えるだけで、CSS 側で定義されたすべてに一瞬でテーマカラーを適用できます。
4. ✍️ イミュータブルな配列操作:React の原則の遵守
React で状態 (state) を安全に更新するためには、「元のデータを直接変更してはいけない(イミュータブル)」という厳格なルールがあります。このアプリでは、すべてのタスク操作でこのルールを守っています。
実現方法:
タスクの追加: push() は使わず、スプレッド構文 (...todos) で既存の要素を展開し、新しい配列を作成しています。
// src/App.jsx
setTodos([newTodo, ...todos]); // 新しい配列を作成してセット
タスクの完了/更新:
todos.map() を使い、ID が一致する要素だけ新しいオブジェクト {...todo, completed: !todo.completed} に置き換えた新しい配列を返します。
// src/App.jsx
setTodos(
todos.map((todo) =>
// IDが一致した場合だけ、新しいオブジェクトを返す
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
タスクの削除:
todos.filter() を使い、削除したい要素 (todo.id !== id) 以外の要素だけで構成された新しい配列を返します。
// src/App.jsx
setTodos(todos.filter((todo) => todo.id !== id));
5. 🛡️ Electron のセキュリティ設計:IPC 通信と Preload
このアプリが通常のウェブアプリではなく、デスクトップアプリ(Electron)であることに関わる重要なセキュリティと通信の仕組みです。
課題:
Electron のレンダラープロセス(React が動く画面)から、ファイルシステムなどのデスクトップ機能に直接アクセスさせるのは非常に危険です。
解決策:
「メインプロセス」と「レンダラープロセス」を分離し、preload.cjs を使って安全な通信路だけを開きます。
実現方法:
1) 隔離設定
electron/main.cjs のウィンドウ作成時に contextIsolation: true を設定し、セキュリティを確保します。
2) 橋渡し役 (Preload)
electron/preload.cjs で contextBridge を使用し、window.electronAPI という名前で、テーマ変更機能 (updateTitleBarOverlay) のみを安全に公開します。
// electron/preload.cjs (抜粋)
contextBridge.exposeInMainWorld("electronAPI", {
updateTitleBarOverlay: (theme) => ipcRenderer.send("update-title-bar-overlay", theme),
});
3) 安全な通信 (IPC)
React 側 (src/App.jsx) は window.electronAPI 経由でメッセージを送信し、メインプロセス (electron/main.cjs) が ipcMain.on でそれを受信して、タイトルバーのボタンの色を変える処理を実行します。
5.アプリの動作フロー
起動時の流れ
タスク追加の流れ
タスク編集の流れ
タスク完了/削除の流れ
完了切り替え:
削除:
テーマ切り替えの流れ
6.主要な技術とライブラリ
React Hooks
| Hook | 用途 | このアプリでの使い方 |
|---|---|---|
useState |
状態管理 | タスクリスト、入力値、テーマなどを管理 |
useEffect |
副作用処理 | LocalStorage への保存、テーマの適用 |
useRef |
DOM 参照 | テキストエリアの高さ自動調整 |
Framer Motion(アニメーション)
<motion.div
initial={{ opacity: 0, y: 20 }} // 最初は透明で下にある
animate={{ opacity: 1, y: 0 }} // 不透明で元の位置に移動
exit={{ opacity: 0, x: -50 }} // 削除時は左に移動しながら消える
layout // 位置変更時に自動アニメーション
>
LocalStorage(データ永続化)
// 保存
localStorage.setItem("todos", JSON.stringify(todos));
// 読み込み
const saved = localStorage.getItem("todos");
const todos = saved ? JSON.parse(saved) : [];
Lucide React(アイコン)
import { Plus, Trash2, Check, Calendar } from "lucide-react";
<Plus size={24} />; // 24pxのプラスアイコン
CSS 変数(テーマ管理)
/* 定義 */
:root {
--accent-color: #38bdf8;
}
/* 使用 */
.button {
background: var(--accent-color);
}
7.覚えておきたい基本ポイントのまとめ
JavaScript の重要な概念
配列のメソッド
// map(): 各要素を変換した新しい配列を作成
const numbers = [1, 2, 3];
const doubled = numbers.map((n) => n * 2); // [2, 4, 6]
// filter(): 条件に合うものだけの新しい配列を作成
const evenNumbers = numbers.filter((n) => n % 2 === 0); // [2]
// sort(): 配列を並び替え(元の配列を変更するので注意!)
const sorted = [...numbers].sort((a, b) => b - a); // [3, 2, 1]
三項演算子
// 条件 ? 真の場合 : 偽の場合
const icon = theme === "dark" ? <Sun /> : <Moon />;
// if文と同じ意味
let icon;
if (theme === "dark") {
icon = <Sun />;
} else {
icon = <Moon />;
}
スプレッド構文
// 配列のコピー
const original = [1, 2, 3];
const copy = [...original]; // [1, 2, 3]
// 配列の結合
const combined = [...newTodo, ...todos];
// オブジェクトのコピーと一部変更
const updated = { ...todo, completed: true };
アロー関数
// 従来の書き方
function add(a, b) {
return a + b;
}
// アロー関数の書き方
const add = (a, b) => a + b;
// 複数行の場合
const add = (a, b) => {
const result = a + b;
return result;
};
分割代入
// 配列の分割代入
const [first, second] = [1, 2]; // first=1, second=2
// useStateでよく使う
const [value, setValue] = useState("");
// オブジェクトの分割代入
const { id, text, completed } = todo;
React の重要な概念
状態(State)
コンポーネント内で管理するデータ。値が変わると自動的に再描画される。
const [count, setCount] = useState(0);
// ダメな例(状態を直接変更)
count = 5; // ❌ 再描画されない
// 正しい例(set関数を使用)
setCount(5); // ✅ 再描画される
イミュータブル(不変性)
React では元のデータを変更せず、新しいデータを作る。
// ダメな例
todos.push(newTodo); // ❌ 元の配列を変更
setTodos(todos);
// 正しい例
setTodos([...todos, newTodo]); // ✅ 新しい配列を作成
イベントハンドラ
ユーザーの操作に反応する。
// クリックイベント
<button onClick={() => deleteTodo(id)}>削除</button>
// 入力イベント
<input onChange={(e) => setValue(e.target.value)} />
// キーボードイベント
<input onKeyDown={(e) => {
if (e.key === 'Enter') {
addTodo()
}
}} />
条件付きレンダリング
// &&演算子: 条件が真の時だけ表示
{
completedCount > 0 && <button>Clear</button>;
}
// 三項演算子: 条件によって切り替え
{
todos.length === 0 ? <p>空です</p> : <List />;
}
Electron の重要な概念
メインプロセスとレンダラープロセス
- メインプロセス: ウィンドウを管理する「裏方」
- レンダラープロセス: UI を表示する「表舞台」
- それぞれ独立しているため、通信が必要
IPC 通信(プロセス間通信)
// レンダラー → メイン(preload.cjs経由)
window.electronAPI.updateTitleBarOverlay(theme);
// メインプロセスで受信(main.cjs)
ipcMain.on("update-title-bar-overlay", (event, theme) => {
// 処理
});
セキュリティ設定
webPreferences: {
nodeIntegration: false, // Node.js APIを無効化(安全)
contextIsolation: true, // コンテキストを分離(安全)
preload: path.join(__dirname, 'preload.cjs') // 橋渡し
}