この7日間のゴールと全体像
この 7 日間は「ToDoアプリ」を題材に、ただ動くものではなく「設計できる人」になることを狙います。
テーマは次の 3 つです。
- 状態管理(state):アプリの「今の状態」をどう持つか
- データ構造設計:タスクを「配列+オブジェクト」でどう表現するか
- 再描画ロジック:状態が変わったときに画面をどう描き直すか
技術的には「ライブラリなしの素の JavaScript(バニラ JS)」で書きます。
ただし考え方は、そのまま React / Vue などのフレームワークに直結します。
毎日 120 分は、ざっくり「60 分で理解」「60 分で手を動かす」をイメージしてください。
1 日目:状態(state)とデータ構造の設計を固める
ToDoアプリの「状態」とは何か
まず、ToDo アプリが「今どうなっているか」を言葉にします。
この情報の集まりが state(状態)です。
状態として必要なものは最低でも次です。
- 画面に存在するタスクの一覧
- タスクごとの情報(id, title, completed)
- 今のフィルタ状態(全件 / 完了 / 未完了)
ここで大事なのは、「DOM(HTML 要素)を状態として持たない」ことです。
状態は「純粋なデータ」として JavaScript の変数に持ち、画面は「状態の結果」として描画します。
タスクのデータ構造を決める(重要部分)
タスク 1 件をオブジェクトで表現します。
const task = {
id: 1,
title: "牛乳を買う",
completed: false,
};
JavaScriptこの 3 つにはそれぞれ理由があります。
- id
削除・完了切り替えで「どのタスクか」を識別するための一意な番号。
配列のインデックスを直接使うと、削除時などにズレてややこしくなるので、id を持たせる方が安全です。 - title
タスクの内容。文字列で OK。 - completed
true なら完了、false なら未完了。
「完了日」などを後から追加したい場合も、このフラグは土台として生き続けます。
複数のタスクは配列にします。
let tasks = [
{ id: 1, title: "牛乳を買う", completed: false },
{ id: 2, title: "メール返信", completed: true },
];
JavaScriptこれで「配列+オブジェクト」の構造になります。
全体の state オブジェクトを定義する
状態をひとまとめにしたオブジェクトを用意します。
const state = {
tasks: [],
filter: "all", // "all" | "done" | "todo"
};
JavaScriptstate の中に
- tasks(全タスク)
- filter(表示モード)
を入れることで、「アプリの状態は state を見ればわかる」形にします。
この「状態を 1 カ所に集める」という考え方が、設計力を一気に上げてくれます。
2 日目:HTML の骨組みと「状態から再描画する」関数の雛形
最小限の HTML を用意する
index.html を次のように用意します(CSS はかなり簡略化)。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<title>ToDo アプリ</title>
<style>
body { font-family: sans-serif; padding: 20px; }
.todo-item { display: flex; align-items: center; margin-bottom: 4px; }
.todo-title { flex-grow: 1; margin-left: 8px; }
.todo-title.completed { text-decoration: line-through; color: #888; }
button { margin-left: 4px; }
.filters button.active { font-weight: bold; }
</style>
</head>
<body>
<h1>ToDo アプリ</h1>
<div>
<input id="todo-input" type="text" placeholder="タスクを入力..." />
<button id="add-button">追加</button>
</div>
<div class="filters">
<button data-filter="all" class="active">全件</button>
<button data-filter="todo">未完了</button>
<button data-filter="done">完了</button>
</div>
<div id="todo-list"></div>
<script src="app.js"></script>
</body>
</html>
ポイントは、JavaScript から触る要素に id や data 属性を付けておくことです。
JavaScript 側の基本構造
app.js に次を書きます。
const state = {
tasks: [],
filter: "all",
};
const inputEl = document.getElementById("todo-input");
const addButtonEl = document.getElementById("add-button");
const listEl = document.getElementById("todo-list");
const filterButtons = document.querySelectorAll(".filters button");
let nextId = 1;
JavaScriptここまでは「状態と DOM を掴む」ところまで。
次は「state から DOM を再描画する render 関数」を作ります。
render 関数の雛形(重要)
function render() {
listEl.textContent = "";
const filteredTasks = getFilteredTasks();
filteredTasks.forEach((task) => {
const itemEl = document.createElement("div");
itemEl.className = "todo-item";
const checkboxEl = document.createElement("input");
checkboxEl.type = "checkbox";
checkboxEl.checked = task.completed;
const titleEl = document.createElement("div");
titleEl.className = "todo-title";
titleEl.textContent = task.title;
if (task.completed) {
titleEl.classList.add("completed");
}
const deleteButtonEl = document.createElement("button");
deleteButtonEl.textContent = "削除";
itemEl.appendChild(checkboxEl);
itemEl.appendChild(titleEl);
itemEl.appendChild(deleteButtonEl);
listEl.appendChild(itemEl);
});
updateFilterButtons();
}
JavaScriptこの render のポイントを深掘りします。
- listEl.textContent = “”
一度リストを空にしてから、state に基づいて全て描き直す。
「部分更新」よりシンプルでバグりにくい設計です。 - filteredTasks.forEach で、現在のフィルタに応じたタスクだけを描画。
「表示対象を決めるロジック」と「DOM を作るロジック」を分けやすくなります。 - DOM 生成は、あくまで state のコピー。
DOM に書き換えた結果を「真実」とみなさず、state を真実とみなします。
この「状態から毎回全部描画」が、設計力強化の核になります。
3 日目:タスク追加ロジックと「状態を書き換えてから render」の流れ
タスクを追加する関数
タスク追加は、「state を更新する関数」に切り出します。
function addTask(title) {
const trimmed = title.trim();
if (!trimmed) {
return;
}
const newTask = {
id: nextId++,
title: trimmed,
completed: false,
};
state.tasks.push(newTask);
}
JavaScriptここで意識してほしいのは、
- DOM に直接新しい要素を追加しない
- 必ず「state.tasks に push → render()」の順にする
という流れです。
イベントハンドラから addTask を呼び出す
addButtonEl.addEventListener("click", () => {
addTask(inputEl.value);
inputEl.value = "";
render();
});
inputEl.addEventListener("keydown", (event) => {
if (event.key === "Enter") {
addTask(inputEl.value);
inputEl.value = "";
render();
}
});
JavaScriptここでも「状態変更 → 再描画」です。
- addTask で state を変える
- render で画面を状態に合わせる
イベントハンドラの中に DOM 操作を直接書かず、「状態変化の窓口」だけ通るようにする。
これが中級に上がるための感覚です。
getFilteredTasks の実装
render 内で使っていた getFilteredTasks を実装します。
function getFilteredTasks() {
if (state.filter === "all") {
return state.tasks;
} else if (state.filter === "done") {
return state.tasks.filter((task) => task.completed);
} else if (state.filter === "todo") {
return state.tasks.filter((task) => !task.completed);
}
}
JavaScriptここでは
- データそのもの(state.tasks)は変えない
- 表示用に「フィルタ済みの配列」を返す
という設計にしています。
フィルタは「ビューの問題」であり、「データの削除」ではないことを意識してください。
4 日目:完了切り替え・削除ロジックとイベントの紐付け
完了状態の切り替えロジック(重要)
「チェックボックスをクリックしたら、対応するタスクの completed を反転」
という動きを作ります。
ロジック自体はシンプルです。
function toggleTaskCompleted(id) {
const task = state.tasks.find((t) => t.id === id);
if (!task) return;
task.completed = !task.completed;
}
JavaScriptここでのポイントは
- 「どのタスクか」は id で特定する
- 配列の index に依存しない
ことです。
index ベースで操作すると、削除・並び替えのたびに壊れやすくなります。
削除ロジック
削除は「指定 id のタスクを除いた新しい配列を作る」書き方にします。
function deleteTask(id) {
state.tasks = state.tasks.filter((t) => t.id !== id);
}
JavaScriptfilter を使って、条件に合うものだけ残すパターンです。
この書き方は「元の配列を直接いじる」のではなく、「新しい配列を作って置き換える」ので、
状態の変化が読みやすくなります。
DOM イベントとタスク ID の紐付け
問題は、「クリックされた DOM と、タスクの id をどう紐付けるか」です。
中級としては「data 属性を使う」のがきれいです。
render を少し修正します。
filteredTasks.forEach((task) => {
const itemEl = document.createElement("div");
itemEl.className = "todo-item";
itemEl.dataset.id = String(task.id); // ここで紐付け
const checkboxEl = document.createElement("input");
checkboxEl.type = "checkbox";
checkboxEl.checked = task.completed;
const titleEl = document.createElement("div");
titleEl.className = "todo-title";
titleEl.textContent = task.title;
if (task.completed) {
titleEl.classList.add("completed");
}
const deleteButtonEl = document.createElement("button");
deleteButtonEl.textContent = "削除";
itemEl.appendChild(checkboxEl);
itemEl.appendChild(titleEl);
itemEl.appendChild(deleteButtonEl);
listEl.appendChild(itemEl);
});
JavaScriptイベントハンドラは「イベント委譲」を使うときれいです。
親要素に 1 個だけ付けて、子要素で何が起こったかを見るパターンです。
listEl.addEventListener("click", (event) => {
const target = event.target;
if (target.tagName === "INPUT" && target.type === "checkbox") {
const itemEl = target.closest(".todo-item");
const id = Number(itemEl.dataset.id);
toggleTaskCompleted(id);
render();
}
if (target.tagName === "BUTTON") {
const itemEl = target.closest(".todo-item");
const id = Number(itemEl.dataset.id);
deleteTask(id);
render();
}
});
JavaScriptここが中級ポイントです。
- DOM の構造(closest など)を使って、「どのタスクの要素か」を見つける
- data-id から id を取り出して、state を更新
- そのあと render で再描画
「DOM → id → state」という経路を辿っていることを意識してください。
5 日目:フィルタボタンと「状態と UI の同期」
フィルタ状態を変更するロジック
フィルタは単純で、state.filter を変えるだけです。
function setFilter(filter) {
state.filter = filter;
}
JavaScriptただし、値は “all” / “todo” / “done” のみとします。
フィルタボタンにイベントを付ける
HTML 側で、ボタンに data-filter 属性を付けているので、それを使います。
filterButtons.forEach((button) => {
button.addEventListener("click", () => {
const filter = button.dataset.filter;
setFilter(filter);
render();
});
});
JavaScriptボタンの active クラスを切り替えるのが「UI 側の状態」です。
これも render の中で state.filter から決めるようにします。
function updateFilterButtons() {
filterButtons.forEach((button) => {
const filter = button.dataset.filter;
if (filter === state.filter) {
button.classList.add("active");
} else {
button.classList.remove("active");
}
});
}
JavaScriptここでまた「状態(state.filter)から UI を決める」パターンが出てきました。
- フィルタボタンを押す
- state.filter を更新する
- render() が呼ばれ、updateFilterButtons() の中で active クラスが付け替わる
ボタン自体に「自分が active かどうか」を記憶させない。
あくまで state を見て「あなたは active / inactive」と決める。
この一方向の流れが、設計をシンプルにしてくれます。
6 日目:コード全体を整理して「役割」を明確にする
関数の役割を言葉にする(重要)
ここまでで増えてきた関数を、役割別に整理します。
状態を変える関数(state を書き換える側):
- addTask(title)
- toggleTaskCompleted(id)
- deleteTask(id)
- setFilter(filter)
状態から UI を作る関数(state を読むだけの側):
- render()
- getFilteredTasks()
- updateFilterButtons()
イベントを受けて橋渡しをする関数(UI と状態をつなぐ):
- addButton / input / list / filterButtons に付けたイベントハンドラたち
この 3 層を意識しておくと、
「状態をいじるロジックが UI 側に紛れ込んでないか」
「DOM 操作が state 操作の中に紛れ込んでないか」
というチェックができるようになります。
初期データを入れて動作確認
テストのために、最初からいくつかタスクを入れておくと確認しやすいです。
state.tasks = [
{ id: nextId++, title: "サンプルタスク1", completed: false },
{ id: nextId++, title: "サンプルタスク2", completed: true },
];
render();
JavaScriptこの状態で、
- 追加
- 完了切り替え
- 削除
- フィルタ切り替え
を一通り試し、挙動がおかしいところを見つけたら
「state がどうなっていて、それを render がどう解釈しているか」
という観点でデバッグしてみてください。
7 日目:設計の観点で振り返りと応用の入り口
この ToDo アプリの設計を言語化する
今回の ToDo アプリは、ざっくり言うとこういう設計です。
- 真実の情報は state にだけ持つ(tasks 配列と filter 文字列)
- ユーザー操作は、必ず「state を変える関数」を通る
- state が変わったら、必ず render() で UI を描き直す
- render は「state を読むだけ」であって、state をいじらない
- フィルタは「表示ロジック(getFilteredTasks)」でのみ適用する
これをもっと抽象的に言うと、
- 状態(model)
- 描画(view)
- イベント処理(controller)
のような役割に分かれている、と見ることもできます。
MVC や Flux、React の「単方向データフロー」などの土台になっている考え方です。
応用アイデア:設計を崩さずに機能を足す
例えば次のような機能を追加したくなったとします。
- 期限(dueDate)を追加したい
- タスクに「重要フラグ」を付けたい
- 「未完了の件数」をヘッダーに表示したい
今回の設計なら、どれも次のように拡張できます。
期限を付けるなら:
- Task の構造に dueDate フィールドを追加
- addTask で dueDate を受け取る
- render の中で表示を追加
- フィルタやソート条件に dueDate を使いたければ、getFilteredTasks を拡張
重要フラグなら:
- Task に isImportant を追加
- toggleImportant(id) のような state 更新関数を作る
- render で重要タスクにマークを付けて表示
どの場合も、
- state の構造を変える
- state を変える関数を足す
- state から描画するロジックを足す
という流れで済むはずです。
逆に「DOM に直接フラグを持たせてしまう」設計にしていたら、この拡張は一気に苦しくなります。
まとめ:この 7 日間で鍛えた「設計の筋肉」
この ToDo アプリ【設計力強化編】で、あなたは次の感覚を手に入れています。
- アプリの「状態(state)」をデータ構造として設計する
- タスクを「配列+オブジェクト」で表現し、id で操作する
- 「状態を書き換える関数」と「状態から UI を作る関数」をきっちり分ける
- 「状態が変わったら render で全部描き直す」という単純で強いパターンを使える
- フィルタを「データを壊さず表示だけ変える」ロジックとして設計する
これは、そのまま React や Vue を学ぶときに
「なんで state を一元管理するのか」「なんでコンポーネントは props から描画するのか」
を理解する土台になります。

