JavaScript / LESSON 17 / JS17

JavaScript:ローカルストレージと状態管理

1学習の目的

2基礎解説

やりたいこと書き方備考
保存するlocalStorage.setItem("key", 値)文字列しか保存できない
取り出すlocalStorage.getItem("key")無ければ null
オブジェクトを保存JSON.stringify(obj)文字列に変換してから
オブジェクトを復元JSON.parse(文字列)元の形に戻す
✅ 覚えるべき重要ポイント
  1. 保存できるのは文字列だけ。オブジェクトをそのまま渡すと [object Object] という文字列になり、中身が失われる。必ず JSON.stringify() を通す。
  2. 数値を保存しても取り出すときは文字列になる。計算に使うなら Number() で変換する(JS02)。
  3. 存在しないキーは nullJSON.parse(null) は null を返すので、?? 既定値 で受けるのが安全。
  4. 保存されているのが壊れた文字列だと JSON.parseSyntaxError を投げる。読み込みは try で囲む(JS10・JS16と同じ)。
  5. 状態を1箇所で持ち、変わったら「保存 → 再描画」を必ずセットで行う。これを守れば、画面と保存内容がズレることがなくなる。

現場使用例:ダークモードの設定、入力途中の下書き保存、閲覧履歴、ログイン状態の保持、ToDoやカートの中身。サーバーを使わずにデータを残せるのが最大の利点。

// 文字列はそのまま保存できる
localStorage.setItem("theme", "dark");
console.log(localStorage.getItem("theme"));     // dark

// オブジェクトは JSON にしてから
const todos = [{ text: "買い物", done: false }];
localStorage.setItem("todos", JSON.stringify(todos));

// 読み込み(無い場合や壊れている場合に備える)
function loadTodos() {
  try {
    return JSON.parse(localStorage.getItem("todos")) ?? [];
  } catch (e) {
    return [];
  }
}

console.log(loadTodos());     // [{ text: '買い物', done: false }]

// よくある間違い:変換せずに保存する
localStorage.setItem("bad", { a: 1 });
console.log(localStorage.getItem("bad"));       // [object Object]

3基本ドリル(10問)

JS17-D01 localStorage に キー "name"、値 "田中" を保存し、取り出して出力せよ。 コーディング★☆☆無料
期待される結果

出力 == 田中

ヒント

setItem で保存、getItem で取得する。

模範解答
localStorage.setItem("name", "田中");
console.log(localStorage.getItem("name"));
解説

ブラウザを閉じても残るのが localStorage。同じサイトを再訪したとき、前回の状態から続けられる。

JS17-D02 localStorage.setItem("data", { a: 1 })オブジェクトをそのまま保存するとどうなるか。
A. 正しく保存される B. "[object Object]" という文字列になる C. エラーになる D. null が保存される
選択★☆☆無料
期待される結果

解答 == B

ヒント

localStorage が保存できるのは何型か。

模範解答
B
解説

中身が完全に失われるのに、エラーも出ないのが厄介。取り出したときに初めて気づくことになる。必ず JSON.stringify() を通す。

JS17-D03 存在しないキー "missing" を取り出し、値が無ければ「未設定」と出力せよ(?? を使うこと)。 コーディング★☆☆無料
期待される結果

出力 == 未設定

ヒント

getItem は無ければ null を返す。?? で既定値を指定できる(JS03)。

模範解答
console.log(localStorage.getItem("missing") ?? "未設定");
解説

??null と undefined のときだけ右側を返す。空文字を有効な値として扱いたい場合、|| より安全(JS03)。

JS17-D04 オブジェクトを保存できる形に変換するコードを完成させよ。 穴埋め★☆☆無料
コード
const user = { name: "田中", age: 30 };
localStorage.setItem("user", JSON.____(user));
console.log(localStorage.getItem("user"));
期待される結果

出力 == {"name":"田中","age":30}

ヒント

オブジェクトをJSON文字列にするメソッド(JS16でも使った)。

模範解答
stringify
解説

配列でもオブジェクトでも、入れ子になっていてもそのまま文字列にできる。取り出すときは JSON.parse で元に戻す。

JS17-D05 localStorage.setItem("count", 42) のあと getItem で取り出した値の型はどれか。
A. number B. string C. object D. undefined
選択★☆☆無料
期待される結果

解答 == B

ヒント

localStorage は何型で保存するか。

模範解答
B
解説

取り出すと "42" という文字列になる。そのまま計算すると "42" + 1 が "421" になるので、Number() での変換が必須(JS02)。

JS17-D06 配列 ["買い物", "掃除"] を localStorage に保存し、取り出して配列として復元し、要素数を出力せよ。 コーディング★★☆無料
期待される結果

出力 == 2

ヒント

保存は JSON.stringify、復元は JSON.parse。復元後は普通の配列として扱える。

模範解答
const todos = ["買い物", "掃除"];

localStorage.setItem("todos", JSON.stringify(todos));

const loaded = JSON.parse(localStorage.getItem("todos"));
console.log(loaded.length);
解説

stringify と parse は往復の関係。復元した配列には push も map もそのまま使える(JS06・JS09)。

JS17-D07 保存された文字列を元のオブジェクトに戻すコードを完成させよ。 穴埋め★★☆無料
コード
localStorage.setItem("user", '{"name":"田中"}');

const user = JSON.____(localStorage.getItem("user"));
console.log(user.name);
期待される結果

出力 == 田中

ヒント

JSON文字列を解析してオブジェクトにするメソッド。

模範解答
parse
解説

parse を通さないと user.name は undefined になる。文字列のままではプロパティにアクセスできない。

JS17-D08 localStorage に保存された値が壊れていて JSON.parse が失敗したとき、適切な対処はどれか。
A. そのまま使う B. try/catch で受け止めて既定値を返す C. 無視する D. ページを再読み込みする
選択★★☆無料
期待される結果

解答 == B

ヒント

JSON.parse は失敗すると例外を投げる。

模範解答
B
解説

保存内容はユーザーが手で書き換えられるし、アプリの更新で形式が変わることもある。読み込みは必ず try で囲み、失敗したら初期状態から始める。

JS17-D09 キー "count" に保存された数値("5")を取り出して1増やして保存し直し、結果を出力せよ。保存が無い場合は 0 から始めること。 コーディング★★☆無料
期待される結果

出力 == 6

ヒント

Number() で変換する。保存が無い場合は ?? "0" で受けるか、Number(null) が 0 になる性質を使う。

模範解答
localStorage.setItem("count", "5");

const current = Number(localStorage.getItem("count") ?? 0);
const next = current + 1;

localStorage.setItem("count", next);
console.log(localStorage.getItem("count"));
解説

取り出す → 変換 → 計算 → 保存し直すが更新の基本形。訪問回数のカウントなどはこの4手で作れる。

JS17-D10 キー "temp" に値を保存したあと削除し、削除後に取り出した結果を出力せよ(null になることを確認する)。 コーディング★★☆無料
期待される結果

出力 == null

ヒント

removeItem で1件だけ削除できる。全部消すなら clear()。

模範解答
localStorage.setItem("temp", "一時データ");
localStorage.removeItem("temp");

console.log(localStorage.getItem("temp"));
解説

clear()そのサイトの保存を全部消すので扱いに注意。通常は removeItem でキーを指定して消す。

4実践シナリオ(5問)

JS17-S01 【安全な読み込み関数】キーを受け取り、保存された配列を返す関数 loadArray を定義せよ。保存が無い場合・壊れている場合はどちらも空配列を返すこと。
正常なデータ、未保存のキー、壊れたデータの3通りで試し、「2件」「0件」「0件」の3行を出力せよ。
コーディング★★☆無料
期待される結果

出力3行が完全一致

ヒント

try の中で JSON.parse し、?? [] で null に備える。catch でも [] を返す。

模範解答
function loadArray(key) {
  try {
    return JSON.parse(localStorage.getItem(key)) ?? [];
  } catch (e) {
    return [];
  }
}

localStorage.setItem("ok", JSON.stringify(["A", "B"]));
localStorage.setItem("broken", "{壊れた");

console.log(`${loadArray("ok").length}件`);
console.log(`${loadArray("missing").length}件`);
console.log(`${loadArray("broken").length}件`);
解説

どんな状態でも必ず配列が返るので、呼び出し側は null チェックをしなくて済む。この「常に安全な形で返す」設計が、アプリ全体を壊れにくくする。

JS17-S02 【設定の保存】設定オブジェクト { theme: "dark", fontSize: 16 } を保存し、読み込んで「テーマ:dark / 文字サイズ:16」と出力せよ。そのあと theme を "light" に変更して保存し直し、再度読み込んで「テーマ:light / 文字サイズ:16」と出力すること。 コーディング★★☆無料
期待される結果

出力2行が完全一致

ヒント

読み込み→変更→保存し直しの流れ。スプレッド構文 { ...settings, theme: "light" } で一部だけ変えられる。

模範解答
const save = (settings) => localStorage.setItem("settings", JSON.stringify(settings));
const load = () => JSON.parse(localStorage.getItem("settings")) ?? {};

save({ theme: "dark", fontSize: 16 });

const first = load();
console.log(`テーマ:${first.theme} / 文字サイズ:${first.fontSize}`);

save({ ...first, theme: "light" });

const second = load();
console.log(`テーマ:${second.theme} / 文字サイズ:${second.fontSize}`);
解説

保存と読み込みを関数にまとめると、キー名やJSON変換を何度も書かずに済む。{ ...first, theme: "light" } は他の項目を保ったまま1つだけ上書きする書き方(JS07)。

JS17-S03 【訪問回数のカウント】キー "visits" に訪問回数を保存する処理を書け。初回は1、以降は1ずつ増える。3回連続で実行して「1回目の訪問です」「2回目の訪問です」「3回目の訪問です」の3行を出力せよ。 コーディング★★☆無料
期待される結果

出力3行が完全一致

ヒント

関数にして3回呼ぶ。取り出した値は文字列なので Number() で変換する。

模範解答
function visit() {
  const count = Number(localStorage.getItem("visits") ?? 0) + 1;
  localStorage.setItem("visits", count);
  console.log(`${count}回目の訪問です`);
}

visit();
visit();
visit();
解説

Number() を忘れると "01" "011" と文字列が連結されていく。localStorage から取り出した値を計算に使うときは、必ず変換する。

JS17-S04 【状態を1箇所で持つ】ToDoの配列を状態として持ち、追加のたびに保存する設計にせよ。
関数 addTodo(text) を作り、① 状態に追加 ② localStorage に保存 ③ 「追加:買い物(全1件)」と出力、の3つを行うこと。「買い物」「掃除」を順に追加し、最後に保存内容を読み直して「保存済み:2件」と出力せよ。
コーディング★★★無料
期待される結果

出力3行が完全一致

ヒント

todos 配列を上に置き、addTodo の中で push → 保存 → 出力の順に行う。最後は JSON.parse で読み直す。

模範解答
const KEY = "todos";
const todos = JSON.parse(localStorage.getItem(KEY) ?? "[]");

function save() {
  localStorage.setItem(KEY, JSON.stringify(todos));
}

function addTodo(text) {
  todos.push({ text, done: false });
  save();
  console.log(`追加:${text}(全${todos.length}件)`);
}

addTodo("買い物");
addTodo("掃除");

const loaded = JSON.parse(localStorage.getItem(KEY));
console.log(`保存済み:${loaded.length}件`);
解説

状態を変えたら必ず保存する——この順序を徹底することが要点。保存を忘れる箇所が1つでもあると、画面と保存内容がズレて原因の分かりにくいバグになる。

JS17-S05 【状態から画面を描く】ToDoの状態と、それを画面に反映する関数 render を作れ。#list(ul)と #summary(p)があるものとする。
render は ① list の中身を空にする ② 状態から li を作る(done なら "done" クラス)③ summary に「全2件 / 完了1件」と表示する。
「買い物(完了)」「掃除」の2件を状態に入れて render を呼び、li が2つ・うち1つに done クラス・summary が正しいことを確認せよ。
コーディング★★★無料
期待される結果

li 2つ・done 1つ・summary が 全2件 / 完了1件

ヒント

render の中で list.innerHTML = "" してから作り直す(JS11)。完了数は filter で数える。

模範解答
const todos = [
  { text: "買い物", done: true },
  { text: "掃除", done: false },
];

const list = document.querySelector("#list");
const summary = document.querySelector("#summary");

function render() {
  list.innerHTML = "";

  for (const todo of todos) {
    const li = document.createElement("li");
    li.textContent = todo.text;

    if (todo.done) {
      li.classList.add("done");
    }

    list.appendChild(li);
  }

  const doneCount = todos.filter((t) => t.done).length;
  summary.textContent = `全${todos.length}件 / 完了${doneCount}件`;
}

render();
解説

「状態が正、画面はその写し」という考え方。画面を直接いじるのではなく、状態を変えて render を呼び直す。この形なら画面と状態が食い違うことがなく、ReactやVueも同じ思想で作られている。

5仕上げ課題

JS17-FINAL 【保存されるToDoアプリ】JS12で作ったToDoアプリに保存機能を追加し、再読み込みしても消えないようにせよ。

HTMLには #input / #addBtn / #list / #summary があるものとする。

仕様:
① 起動時に localStorage から状態を読み込む(無い・壊れている場合は空配列)
save() … 状態を localStorage に保存
render() … list を空にして状態から li を作り直す。li は「テキスト+削除ボタン(class="del"、文字は「削除」)」。done なら li に "done" クラス。summary に「全2件 / 完了1件」を表示
追加:入力値で状態に { text, done: false } を追加(空文字は無視)→ 保存 → 再描画 → 入力欄をクリア
完了:li のテキスト部分をクリックで done を反転 → 保存 → 再描画(イベント委譲)
削除:削除ボタンで状態から取り除く → 保存 → 再描画(イベント委譲)

検証の流れ:「買い物」を追加 → 「掃除」を追加 → 1件目を完了 → 2件目を削除
期待される最終状態:li が1つ・done クラスあり・summary が「全1件 / 完了1件」・localStorage にも同じ内容が保存されている
期待される結果

li 1つ・done あり・summary 一致・保存内容も一致

ヒント

状態を変える操作はすべて「状態を変更 → save() → render()」の3手をセットで行う。何番目の項目かは、li の位置([...list.children].indexOf(li))で特定できる。イベント委譲は e.target が del クラスかどうかで分岐する(JS12)。

模範解答
const KEY = "todos";

const input = document.querySelector("#input");
const addBtn = document.querySelector("#addBtn");
const list = document.querySelector("#list");
const summary = document.querySelector("#summary");

function load() {
  try {
    return JSON.parse(localStorage.getItem(KEY)) ?? [];
  } catch (e) {
    return [];
  }
}

const todos = load();

function save() {
  localStorage.setItem(KEY, JSON.stringify(todos));
}

function render() {
  list.innerHTML = "";

  for (const todo of todos) {
    const li = document.createElement("li");
    li.textContent = todo.text;

    if (todo.done) {
      li.classList.add("done");
    }

    const del = document.createElement("button");
    del.textContent = "削除";
    del.classList.add("del");
    li.appendChild(del);

    list.appendChild(li);
  }

  const doneCount = todos.filter((t) => t.done).length;
  summary.textContent = `全${todos.length}件 / 完了${doneCount}件`;
}

addBtn.addEventListener("click", () => {
  const text = input.value.trim();

  if (!text) {
    return;
  }

  todos.push({ text, done: false });
  save();
  render();
  input.value = "";
});

list.addEventListener("click", (e) => {
  const li = e.target.classList.contains("del") ? e.target.parentNode : e.target;
  const index = [...list.children].indexOf(li);

  if (index === -1) {
    return;
  }

  if (e.target.classList.contains("del")) {
    todos.splice(index, 1);
  } else {
    todos[index].done = !todos[index].done;
  }

  save();
  render();
});

render();
解説

JS12のToDoアプリの弱点が、ついに解消された。ページを閉じても、翌日開いてもデータが残る——ここまで来て初めて「使えるアプリ」になった。

設計の核心は「状態を変更 → save() → render()」を必ずセットで行うことだ。追加・完了・削除のどの操作も、この3手を守っている。もし削除のときだけ save() を忘れたら、画面からは消えるのに再読み込みすると復活する——原因の分かりにくいバグになる。

そしてJS12との決定的な違いは、画面を直接いじっていない点だ。JS12では「li を remove する」と画面を直接操作していたが、今回は状態(todos配列)を変えて render を呼び直すだけ。画面は状態の写しにすぎない。この考え方は「宣言的UI」と呼ばれ、React・Vue・Svelteといった現代のフレームワークすべての土台になっている。あなたはすでにその本質を実装できている。

次章はいよいよ最終回。ここまでの18章すべてを統合して、1本の完成したWebアプリを作る。