データを保存する
localStorageを使ってタスクをブラウザに保存し、リロードしても消えないようにします。
このレッスンで作るもの
ページをリロードしてもタスクが消えないようにします。
ブラウザの localStorage にデータを保存して、次回アクセス時に復元します。
現状、ToDoアプリはページをリロードするとタスクが全部消えてしまいます。(実際に試してみてね)
これはデータが 変数(メモリ) にしか保存されていないためです。ブラウザを閉じたりリロードすると、JavaScriptの変数はリセットされます。
実際のアプリやサービスではデータをデータベースに保存することが多いです。
今回は簡易実装としてでデータ管理を行います。
実務では漏洩しても問題ない情報やUXのためにが利用されることが多いです。 (ダークモードの保存など)
localStorageとは
localStorage は、ブラウザにデータを保存できる仕組みです。
| 特徴 | 説明 |
|---|---|
| 保存場所 | ブラウザ内(ユーザーのPC) |
| データ形式 | 文字列のみ(キーと値のペア) |
| 有効期限 | なし(手動で消すまで残る) |
| 容量 | 約5MB(ブラウザによる) |
主な
| 説明 | 例 | |
|---|---|---|
setItem(key, value) | データを保存 | localStorage.setItem('name', 'tanaka') |
getItem(key) | データを取得 | localStorage.getItem('name') |
removeItem(key) | データを削除 | localStorage.removeItem('name') |
Step 1: 保存するを作る
localStorage は文字列しか保存できないため、配列をに変換して保存します。
// todosをlocalStorageに保存する関数
function saveTodos() {
localStorage.setItem('todos', JSON.stringify(todos));
}JSON変換について
JSON.stringify() はJavaScriptのデータをに変換します。
const data = [{ id: 1, text: "買い物" }];
JSON.stringify(data);
// '[{"id":1,"text":"買い物"}]'(文字列)逆に、JSON.parse() はをJavaScriptのデータに戻します。
const text = '[{"id":1,"text":"買い物"}]';
JSON.parse(text);
// [{ id: 1, text: "買い物" }](配列)Step 2: 起動時にデータを読み込む
ページを開いたときに localStorage からデータを読み込むようにします。
// タスクを保存する配列
let todos = [];
// localStorageからデータを読み込む
const saved = localStorage.getItem('todos');
if (saved) {
todos = JSON.parse(saved);
}
// 次のタスクに割り当てるID
let nextId = todos.length > 0
? Math.max(...todos.map((todo) => todo.id)) + 1
: 1;ポイント
localStorage.getItem('todos')で保存済みデータを取得(なければnull)nullでなければJSON.parse()で配列に変換nextIdは既存タスクの最大IDに1を足した値にする(IDの重複を防ぐ)
Math.max(...todos.map((todo) => todo.id)) は、配列内の全IDから最大値を取得しています。
たとえば todos に id: 1, 3, 5 のタスクがあれば、nextId は 6 になります。
Step 3: データ変更時に保存する
タスクを追加・削除・完了切り替えするたびに saveTodos() を呼びます。
// フォーム送信時の処理
form.addEventListener('submit', (e) => {
e.preventDefault();
const text = input.value.trim();
if (!text) return;
todos.push({ id: nextId, text, completed: false });
nextId++;
renderTodos();
saveTodos(); // ← 追加
input.value = '';
});function deleteTodo(id) {
todos = todos.filter((todo) => todo.id !== id);
renderTodos();
saveTodos(); // ← 追加
}function toggleComplete(id) {
todos = todos.map((todo) => {
if (todo.id === id) {
return { ...todo, completed: !todo.completed };
}
return todo;
});
renderTodos();
saveTodos(); // ← 追加
}動作確認
ブラウザで確認してみましょう。
- タスクをいくつか追加する
- ページをリロード(F5 または Ctrl+R)
- タスクが残っていればOK!
開発者ツールで確認
ブラウザの開発者ツール(F12)を開いて、Application タブ → で保存されたデータを確認できます。
VSCodeのLive Serverでhtmlを開いて、『散歩に行く』 タスクを追加してみます。

に追加されていることを確認できました!
完成コード
ここまでの全体像です。
// HTML要素を取得
const form = document.getElementById('todo-form');
const input = document.getElementById('todo-input');
const list = document.getElementById('todo-list');
// タスクを保存する配列
let todos = [];
// localStorageからデータを読み込む
const saved = localStorage.getItem('todos');
if (saved) {
todos = JSON.parse(saved);
}
// 次のタスクに割り当てるID
let nextId = todos.length > 0
? Math.max(...todos.map((todo) => todo.id)) + 1
: 1;
// todosをlocalStorageに保存する関数
function saveTodos() {
localStorage.setItem('todos', JSON.stringify(todos));
}
// タスクを削除する関数
function deleteTodo(id) {
todos = todos.filter((todo) => todo.id !== id);
renderTodos();
saveTodos();
}
// 完了状態を切り替える関数
function toggleComplete(id) {
todos = todos.map((todo) => {
if (todo.id === id) {
return { ...todo, completed: !todo.completed };
}
return todo;
});
renderTodos();
saveTodos();
}
// タスクを画面に表示する関数
function renderTodos() {
list.innerHTML = '';
todos.forEach((todo) => {
const li = document.createElement('li');
li.className = 'todo-item';
li.innerHTML = `
<input type="checkbox" ${todo.completed ? 'checked' : ''}>
<span>${todo.text}</span>
<button type="button">削除</button>
`;
// チェックボックスにイベントを設定
const checkbox = li.querySelector('input[type="checkbox"]');
checkbox.addEventListener('change', () => {
toggleComplete(todo.id);
});
// 削除ボタンにイベントを設定
const deleteButton = li.querySelector('button');
deleteButton.addEventListener('click', () => {
deleteTodo(todo.id);
});
list.appendChild(li);
});
}
// 初回の画面描画
renderTodos();
// フォーム送信時の処理
form.addEventListener('submit', (e) => {
e.preventDefault();
const text = input.value.trim();
if (!text) {
return;
}
todos.push({ id: nextId, text, completed: false });
nextId++;
renderTodos();
saveTodos();
input.value = '';
});* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background-color: #f5f5f5;
min-height: 100vh;
padding: 40px 20px;
}
.container {
max-width: 480px;
margin: 0 auto;
background: #fff;
border-radius: 8px;
padding: 24px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
h1 {
font-size: 24px;
margin-bottom: 20px;
color: #333;
}
/* フォーム */
#todo-form {
display: flex;
gap: 8px;
margin-bottom: 24px;
}
#todo-input {
flex: 1;
padding: 12px;
font-size: 16px;
border: 1px solid #ddd;
border-radius: 4px;
}
#todo-input:focus {
outline: none;
border-color: #0066ff;
}
#todo-form button {
padding: 12px 20px;
font-size: 16px;
background-color: #0066ff;
color: #fff;
border: none;
border-radius: 4px;
cursor: pointer;
}
#todo-form button:hover {
background-color: #0052cc;
}
/* タスク一覧 */
#todo-list {
list-style: none;
}
.todo-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
border-bottom: 1px solid #eee;
}
.todo-item:last-child {
border-bottom: none;
}
.todo-item input[type="checkbox"] {
width: 20px;
height: 20px;
cursor: pointer;
}
.todo-item span {
flex: 1;
font-size: 16px;
}
/* 完了したタスクのスタイル */
.todo-item input[type="checkbox"]:checked + span {
text-decoration: line-through;
color: #888;
}
.todo-item button {
padding: 4px 8px;
font-size: 14px;
background: none;
border: 1px solid #ddd;
border-radius: 4px;
cursor: pointer;
color: #666;
}
.todo-item button:hover {
background-color: #fee;
border-color: #f66;
color: #c00;
}<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ToDo App</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="container">
<h1>ToDo</h1>
<form id="todo-form">
<input
type="text"
id="todo-input"
placeholder="タスクを入力..."
required
>
<button type="submit">追加</button>
</form>
<ul id="todo-list">
<!-- ここにタスクが追加される -->
</ul>
</div>
<script src="main.js"></script>
</body>
</html>まとめ
localStorage を使って、リロードしてもタスクが消えないようになりました。
次のステップ
ToDoアプリの機能は完成です!しかし、今のままでは自分のPCでしか動きません。
次のレッスンでは、Vercel を使ってアプリをインターネット上に公開します。
友人などにURLを送って、成果物を見てもらいましょう。