Day 50:ToDoアプリ Ver.2
Day 50では、第4フェーズ「ブラウザデータ保存」のまとめとして、ToDoアプリ Ver.2 を作成します。今回のテーマは、前回までに学んだ localStorage、JSON、そして CRUD の考え方を、本格的にToDoアプリへ組み込むことです。追加する機能は、データ保存、ページ再読み込み対応、完了状態保存の3つです。これらは別々の機能に見えますが、実際にはすべて「データをどう持ち、どう保存し、どう画面へ反映するか」という1本の考え方でつながっています。
Day 45では、第3フェーズの総合制作として、DOM操作を中心にしたインタラクティブToDoアプリ Ver.1 を作りました。そこでは、ToDo追加、削除、完了状態、件数表示までを実装しましたが、ページを更新するとデータは消えてしまいました。Day 46からDay 49にかけては、その弱点を補うために、localStorage の基本、入力データ保存、JSONによる配列とオブジェクト保存、そして localStorage を使った CRUD を学んできました。Day 50では、それらをすべて統合し、「ブラウザを再読み込みしても状態が消えないToDoアプリ」へ進化させます。
初心者の方にとって、このDay 50はとても大切な節目です。なぜなら、ここで初めて「ただ動くだけ」ではなく、「状態を持ち続けるアプリ」を自分の手で作れるようになるからです。これはWebアプリ開発の大きな一歩です。今回も、ただ完成コードを出すのではなく、どう考えて機能を足していくのかをステップごとに整理しながら、初学者向けに丁寧に説明していきます。
ToDoアプリ Ver.2 とは何か
ToDoアプリ Ver.2 は、Ver.1 を保存対応に進化させたものです。Ver.1 では、画面上でToDoを追加したり、完了状態を切り替えたり、削除したりすることができました。しかし、ページを再読み込みするとすべて元に戻ってしまいました。Ver.2 では、そこに localStorage を導入することで、ページを再読み込みしてもToDo一覧が残り、さらに完了状態もそのまま復元されるようにします。
今回の追加機能を整理すると、次のようになります。
- データ保存:追加・更新・削除したToDoデータを `localStorage` に保存する
- ページ再読み込み対応:保存されたデータをページ読み込み時に復元して表示する
- 完了状態保存:完了済みか未完了かの状態も保存し、再読み込み後に再現する
つまり、ToDoの「見た目」を保存するのではなく、「データそのもの」を保存し、そのデータから見た目を再構築する考え方です。ここが非常に重要です。
最初に理解したい核心
Day 50で一番大切なのは、画面そのものを保存するわけではない、ということです。初心者の方は、画面に並んでいる <li> 要素をそのまま保存するようなイメージを持ちやすいですが、実際にはそうではありません。保存するのは、ToDoの内容と完了状態を持った「データ」です。
たとえば、次のような配列を保存すると考えます。
const todos = [
{ text: "牛乳を買う", completed: false },
{ text: "JavaScriptを勉強する", completed: true }
];JavaScriptこの配列がアプリの本体です。
- `text` はToDoの内容です。
- `completed` は完了状態です。
画面表示は、この配列をもとに毎回作り直します。つまり、「データが本体、画面は結果」という考え方です。初心者の方は、この考え方をつかめると、保存対応アプリへの理解が一気に深まります。
Ver.2を作る全体の流れ
ToDoアプリ Ver.2 を作る流れを、先に日本語で整理しておきます。コードに入る前に、何をどうするかを明確にしておくことが大切です。
- 最初に `localStorage` から保存済みToDoデータを取り出す
- 保存データがあれば配列に戻す
- その配列をもとに画面へToDo一覧を表示する
- 新しいToDoを追加したら配列へ追加する
- 追加後に `localStorage` へ保存する
- 完了状態を変えたら配列の `completed` を変更する
- 変更後に `localStorage` へ保存する
- 削除したら配列から削除する
- 削除後に `localStorage` へ保存する
- それぞれの操作後に画面を更新する
この流れを見ると、やっていることはかなり一貫しています。つまり、配列を変更し、保存し、再表示する、という流れのくり返しです。このパターンを理解することがDay 50の本質です。
まずはVer.1との違いを整理する
Ver.1 と Ver.2 の違いを比べると、今回何を強化するのかがわかりやすくなります。
- Ver.1 は画面上でだけ動くアプリでした。
- Ver.2 はデータを保存し、再読み込み後も状態を維持するアプリです。
Ver.1では、完了状態の切り替えは classList.toggle() で見た目を変えるだけでした。つまり、その瞬間に表示が変わるだけで、保存はされていませんでした。Ver.2では、見た目を変えるだけでなく、completed の値そのものを変えて保存します。この違いはとても重要です。
初心者の方は、「Ver.1 はDOM中心」「Ver.2 はDOM + データ保存中心」と整理すると理解しやすいです。
最初に必要なHTMLを用意する
まずは、ToDoアプリの基本のHTMLを用意します。Ver.1 と大きくは変わりません。
<h2>ToDoアプリ Ver.2</h2>
<input type="text" id="todoInput" placeholder="ToDoを入力してください">
<button id="addButton">追加</button>
<ul id="todoList"></ul>
このHTMLでは、入力欄、追加ボタン、ToDo一覧を表示するリストがあります。今回のポイントは、HTML自体はあまり変わらず、JavaScript側でのデータ管理が大きく変わることです。初心者の方は、「見た目の土台は同じでも、中の仕組みが進化している」と考えるとよいです。
保存済みデータを読み込むところから始める
Ver.2 では、アプリ開始時にまず localStorage を確認する必要があります。保存されたToDoデータがあれば、それを使って初期表示を作るからです。
// localStorage から保存データを取得する
const savedTodos = localStorage.getItem("todos");
// 保存データがあれば配列に戻し、なければ空配列にする
let todos = savedTodos !== null ? JSON.parse(savedTodos) : [];JavaScriptこの2行はとても大切です。ここで、保存済みデータがある場合はそれを JSON.parse() で元の配列に戻し、ない場合は空配列を使っています。
初心者の方は、ここで「最初から空で始めるのではなく、前回の状態を復元して始める」という発想をつかむことが大切です。
保存処理を関数にまとめる
ToDoアプリ Ver.2 では、追加、完了切替、削除のたびに保存し直します。そのたびに毎回 localStorage.setItem("todos", JSON.stringify(todos)) と書いてもよいのですが、同じ処理が何度も出てくるので関数にまとめると整理しやすいです。
// ToDo 配列を localStorage に保存する関数
function saveTodos() {
localStorage.setItem("todos", JSON.stringify(todos));
}
このように関数化しておくと、あとで saveTodos(); と呼ぶだけで済みます。初心者の方は、「何度も出てくる処理は関数にまとめると見通しがよくなる」と意識すると、コードの整理が上手になっていきます。
表示処理を関数にまとめる
次に、ToDo配列を画面に表示する処理も関数にします。ここがVer.2の中心です。なぜなら、画面は毎回この配列をもとに作り直すからです。
// ToDo を画面に表示する関数
function renderTodos() {
// いったんリストを空にする
todoList.innerHTML = "";
// 配列の内容を1件ずつ表示する
todos.forEach(function (todo, index) {
// li 要素を作成する
const li = document.createElement("li");
// ToDo 文字列を入れる span を作成する
const span = document.createElement("span");
span.textContent = todo.text;
// 完了済みなら見た目を変える
if (todo.completed) {
span.classList.add("completed");
}
// li に文字を追加する
li.appendChild(span);
// リストに追加する
todoList.appendChild(li);
});
}JavaScriptこの時点ではまだボタンを入れていませんが、「配列をもとに画面を描く」という基本形ができています。初心者の方は、ここで「画面は配列から生成されている」という考え方をしっかり意識するとよいです。
完了状態保存の本質を理解する
ここは非常に重要なので深掘りします。完了状態保存とは、見た目の線を保存することではありません。保存するのは completed: true や completed: false というデータです。そして、再読み込み時にそのデータを見て、必要なら completed クラスをつけることで見た目を再現します。
つまり、流れはこうです。
- 完了ボタンを押す
- データの `completed` を変更する
- そのデータを `localStorage` に保存する
- ページを読み込んだとき、保存された `completed` を見る
- `true` なら完了見た目をつける
初心者の方は、「保存するのは見た目ではなく状態データ」という考え方を必ず押さえておきたいです。ここがわかると、今後もっと複雑なUIでも考えやすくなります。
追加処理をVer.2対応にする
追加処理はVer.1と似ていますが、今回は配列へ追加したあとに保存する点が重要です。
// 追加ボタンがクリックされたときの処理
addButton.addEventListener("click", function () {
// 入力内容を取得して空白を除く
const text = todoInput.value.trim();
// 空欄なら追加しない
if (text === "") {
return;
}
// 新しい ToDo オブジェクトを配列に追加する
todos.push({
text: text,
completed: false
});
// 保存する
saveTodos();
// 画面を更新する
renderTodos();
// 入力欄を空にする
todoInput.value = "";
});JavaScriptここで重要なのは、Create のあとに必ず保存と再表示をしていることです。初心者の方は、「配列に入れただけでは画面にも保存にも反映されない」という点を意識すると理解しやすいです。
完了切替処理をVer.2対応にする
完了状態も、今回はデータ本体を更新し、その後に保存・再表示します。
// completed を切り替える
todos[index].completed = !todos[index].completed;
// 保存する
saveTodos();
// 画面を更新する
renderTodos();JavaScriptこれをボタンイベントの中に入れます。ポイントは、classList.toggle() を直接使うのではなく、まずデータを変えることです。画面はその結果として変わります。
初心者の方は、ここで「見た目を直接いじる」のではなく「データを変えてから表示し直す」という発想に慣れることが大切です。
削除処理も保存対応にする
削除処理も基本は同じです。配列から削除し、保存し、再表示します。
// 配列から ToDo を削除する
todos.splice(index, 1);
// 保存する
saveTodos();
// 画面を更新する
renderTodos();JavaScriptこの流れはCreateやUpdateとよく似ています。つまり、ToDoアプリ Ver.2 では、どの操作でも「配列変更 → 保存 → 再表示」という同じパターンをくり返しています。初心者の方は、この共通パターンに気づけると理解がかなり深まります。
完成版コード
ここまでの内容をまとめたToDoアプリ Ver.2 の完成版です。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Day 50 ToDoアプリ Ver.2</title>
<style>
/* 完了済み ToDo の見た目 */
.completed {
text-decoration: line-through;
color: gray;
}
/* リスト項目の余白 */
li {
margin-bottom: 10px;
}
/* ボタンの余白 */
button {
margin-left: 8px;
}
</style>
</head>
<body>
<h2>ToDoアプリ Ver.2</h2>
<input type="text" id="todoInput" placeholder="ToDoを入力してください">
<button id="addButton">追加</button>
<ul id="todoList"></ul>
<script>
// 必要な要素を取得する
const todoInput = document.getElementById("todoInput");
const addButton = document.getElementById("addButton");
const todoList = document.getElementById("todoList");
// localStorage から保存データを取得する
const savedTodos = localStorage.getItem("todos");
// 保存データがあれば配列に戻し、なければ空配列を使う
let todos = savedTodos !== null ? JSON.parse(savedTodos) : [];
// ToDo 配列を localStorage に保存する関数
function saveTodos() {
localStorage.setItem("todos", JSON.stringify(todos));
}
// ToDo を画面に表示する関数
function renderTodos() {
// いったんリストを空にする
todoList.innerHTML = "";
// 配列の内容を1件ずつ表示する
todos.forEach(function (todo, index) {
// li 要素を作成する
const li = document.createElement("li");
// ToDo 文字列を表示する span を作成する
const span = document.createElement("span");
span.textContent = todo.text;
// 完了済みなら見た目を変える
if (todo.completed) {
span.classList.add("completed");
}
// 完了ボタンを作成する
const completeButton = document.createElement("button");
completeButton.textContent = "完了切替";
// 削除ボタンを作成する
const deleteButton = document.createElement("button");
deleteButton.textContent = "削除";
// 完了ボタンの処理
completeButton.addEventListener("click", function () {
// completed の値を反転させる
todos[index].completed = !todos[index].completed;
// 保存する
saveTodos();
// 画面を更新する
renderTodos();
});
// 削除ボタンの処理
deleteButton.addEventListener("click", function () {
// 配列から削除する
todos.splice(index, 1);
// 保存する
saveTodos();
// 画面を更新する
renderTodos();
});
// li に要素を追加する
li.appendChild(span);
li.appendChild(completeButton);
li.appendChild(deleteButton);
// リストに追加する
todoList.appendChild(li);
});
}
// 追加ボタンの処理
addButton.addEventListener("click", function () {
// 入力内容を取得して前後の空白を除く
const text = todoInput.value.trim();
// 空欄なら追加しない
if (text === "") {
return;
}
// 新しい ToDo を追加する
todos.push({
text: text,
completed: false
});
// 保存する
saveTodos();
// 画面を更新する
renderTodos();
// 入力欄を空にする
todoInput.value = "";
});
// ページ読み込み時に保存データを表示する
renderTodos();
</script>
</body>
</html>
この完成版コードの見方
初心者の方は、完成版をいきなり全部理解しようとせず、役割ごとに分けて読むのがおすすめです。
- 保存データ読み込み:`localStorage.getItem()` と `JSON.parse()`
- 保存処理:`saveTodos()`
- 表示処理:`renderTodos()`
- 追加処理:`addButton` のイベント
- 完了状態切替:`completeButton` のイベント
- 削除処理:`deleteButton` のイベント
このように見ると、長いコードではなく「小さな役割が集まったコード」として理解しやすくなります。
初心者が特に注意したいポイント
Day 50では、次のポイントを特に意識すると理解が深まりやすいです。
- 保存するのはHTML要素ではなく、配列とオブジェクトのデータです。
- ページ再読み込み対応には、開始時の読み込み処理が必要です。
- 完了状態保存は、クラスを保存するのではなく `completed` の真偽値を保存します。
- 追加、更新、削除のあとには、保存と再表示が必要です。
- 画面はデータをもとに毎回作り直す、という考え方が大切です。
特に重要なのは、「見た目を直接操作する」のではなく、「データを変えて、結果として見た目を変える」という考え方です。これは初心者が大きく成長するための重要な一歩です。
そのまま使える練習のしかた
このDay 50の内容は、ただ読むだけでは定着しにくいです。実際に手を動かしながら、次の順番で練習すると理解しやすいです。
- まず完成版をそのまま動かす
- ToDoを追加してページ更新し、残っているか確認する
- 完了状態を切り替えてページ更新し、線が残っているか確認する
- 削除してページ更新し、本当に消えているか確認する
- 何も見ずにもう一度最初から作り直す
初心者の方は、特に「ページを更新しても残るか」を何度も確かめると、保存対応の仕組みが実感しやすくなります。
情報セキュリティの観点での注意
今回のToDoアプリのような一般的なタスク管理データであれば、localStorage は学習用にも簡易アプリ用にも便利です。ただし、これまでと同じく、機密情報やパスワードのような重要な情報を保存するのには向いていません。
初心者の方は、localStorage は「便利なブラウザ保存場所」ではあるけれど、「高い機密性が必要な情報の保管場所ではない」と理解しておくことが大切です。
Day 50のゴールを整理する
Day 50では、次のことができるようになっていれば十分です。
- ToDoアプリ Ver.1 と Ver.2 の違いを説明できる
- ToDoデータを配列とオブジェクトで管理できる
- `localStorage` を使ってToDoを保存できる
- ページ再読み込み後にToDoを復元できる
- 完了状態をデータとして保存し、再現できる
- 追加、更新、削除のたびに保存と再表示が必要な理由を説明できる
ここまでできれば、Day 50としてはとても順調です。第4フェーズの学習内容が、実用的なToDoアプリとしてしっかり形になっています。
まとめ
Day 50では、第4フェーズ「ブラウザデータ保存」のまとめとして、ToDoアプリ Ver.2 を作成しました。今回の追加機能は、データ保存、ページ再読み込み対応、完了状態保存の3つでしたが、その本質はすべて「データを本体として管理し、それを localStorage に保存し、画面はそのデータから作り直す」という考え方にあります。これによって、ただその場で動くだけだったToDoアプリが、ページを更新しても状態を保てるアプリへ進化しました。
特に重要なのは、保存するのは見た目ではなくデータだという点です。完了状態保存も、線が引かれた見た目を保存しているのではなく、completed という真偽値を保存し、それをもとに見た目を再現しています。この考え方が身につくと、今後さらに複雑な機能を持つアプリにも対応しやすくなります。
次に進む前に、今回のToDoアプリ Ver.2 を何も見ずに作り直してみるのがおすすめです。そして、そのあとで件数表示の保存や、完了済みだけをまとめる機能などを自分で追加してみると、学習内容がさらに定着しやすくなります。
