tanaka101

データを保存する

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 は文字列しか保存できないため、配列をに変換して保存します。

main.js
// 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 からデータを読み込むようにします。

main.js
// タスクを保存する配列
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から最大値を取得しています。

たとえば todosid: 1, 3, 5 のタスクがあれば、nextId6 になります。

Step 3: データ変更時に保存する

タスクを追加・削除・完了切り替えするたびに saveTodos() を呼びます。

main.js
// フォーム送信時の処理
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 = '';
});
main.js
function deleteTodo(id) {
  todos = todos.filter((todo) => todo.id !== id);
  renderTodos();
  saveTodos(); // ← 追加
}
main.js
function toggleComplete(id) {
  todos = todos.map((todo) => {
    if (todo.id === id) {
      return { ...todo, completed: !todo.completed };
    }
    return todo;
  });
  renderTodos();
  saveTodos(); // ← 追加
}

動作確認

ブラウザで確認してみましょう。

  1. タスクをいくつか追加する
  2. ページをリロード(F5 または Ctrl+R)
  3. タスクが残っていればOK!

開発者ツールで確認

ブラウザの開発者ツール(F12)を開いて、Application タブ → で保存されたデータを確認できます。

VSCodeのLive Serverでhtmlを開いて、『散歩に行く』 タスクを追加してみます。

ローカルストレージの確認

に追加されていることを確認できました!

完成コード

ここまでの全体像です。

main.js 全体
// 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 = '';
});
style.css 全体
* {
  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;
}
index.html 全体
<!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を送って、成果物を見てもらいましょう。