JavaScriptでお気に入り機能を自作!LocalStorageでリロードしても消えない実装法【コピペOK】

javascript
記事内に広告が含まれています。

Webサイト制作をしていて、「この記事をお気に入り登録したい」「この商品を保存しておきたい」といった機能を求められたことはありませんか?

「お気に入り機能」と聞くと、なんだかデータベースやログイン機能の構築が必要で、初心者にはハードルが高そう……と感じてしまうかもしれません。また、いざ自分で書いてみても「ページをリロードすると選択が消えてしまう」「同じアイテムが何度も登録されてしまう」といったトラブルに悩まされることも多いですよね。

実は、JavaScriptの「localStorage」という仕組みを正しく活用すれば、サーバーサイドの知識がなくても、ブラウザにデータを保存し続ける本格的なお気に入り機能を実装できるんです。

この記事では、楽天やAmazonといった有名サイトでも使われている仕組みの解説から、コピペで試せる具体的なコードまで、一歩ずつ丁寧にお伝えします。

この記事を読んでわかること

  • 楽天やAmazonなどの有名サイトが採用している「お気に入り機能」の仕組み
  • localStorage・sessionStorage・Cookieの違いと、お気に入り機能に最適な保存方法
  • HTMLの「data-id」属性を使って、特定のアイテムをJavaScriptで識別する方法
  • JSON形式を使って、お気に入りデータを配列で保存・読み込みするテクニック
  • ページをリロードしても「お気に入り済み」の状態を維持する復元コード
  • 「データが反映されない」「エラーが出る」といった、実装時にハマりやすいバグの回避策

実務の現場はもちろん、ポートフォリオのクオリティをグッと引き上げる「動的なUI」の実装スキルを、ぜひこの機会にマスターしましょう!

JavaScriptでお気に入り機能を作る前に知っておくべき基礎

楽天・Amazon・noteなどのサイトにある「お気に入り機能」の基本的な仕組み

普段何気なく使っている「お気に入り」や「ブックマーク」「ハートボタン」。これらは一体どういう仕組みで動いているのでしょうか。実装に入る前に、まずその構造を正しく理解しておきましょう。

お気に入り機能に必要な3つの要素

お気に入り機能は、大きく分けると次の3つの要素で成り立っています。

  1. UI(ユーザーインターフェース):ハートアイコンやボタンなど、ユーザーが操作する部分
  2. 状態管理:「このアイテムはお気に入り済みかどうか」を判定するロジック
  3. データの永続化:ページを閉じてもお気に入り情報が消えないようにする仕組み

AmazonやYahoo!ショッピングのようなECサイトでは、お気に入り情報はサーバー側のデータベースに保存されています。ログインしているユーザーのIDと商品IDを紐付けて管理するため、どのデバイスからアクセスしても同じお気に入りリストが表示されます。

一方、noteやはてなブックマークのようなコンテンツプラットフォームでも基本的な考え方は同じです。「ユーザーID × コンテンツID」という組み合わせをデータベースに記録し、ページ読み込み時にその情報を取得して、UIに反映させています。

JavaScriptだけで作る場合の違い

バックエンドやデータベースを使わずにJavaScriptだけで実装する場合、データの保存先はブラウザ側になります。代表的な保存先が localStorage です。

この場合のデータの流れは次のようになります。

ユーザーがハートボタンをクリック
        ↓
JavaScriptがクリックを検知(addEventListener)
        ↓
お気に入りリストに追加 or 削除(配列で管理)
        ↓
localStorageに保存(JSON形式で文字列化)
        ↓
ボタンのデザインを変更(CSSクラスの付け外し)

サーバーへの通信が不要なため、シンプルな実装で済むのが最大のメリットです。ECサイトの「比較リスト」や個人ブログの「後で読む」機能など、軽量な用途であればJavaScriptだけで十分実用的なものが作れます。

状態の「オン・オフ」を切り替えるトグル処理

お気に入り機能の核心は、「登録」と「解除」をひとつのボタンで切り替えるトグル処理です。ユーザーがボタンを押すたびに、

  • まだお気に入り登録されていない → 登録する
  • すでにお気に入り登録済み → 解除する

という判定を行います。この判定ロジックと、localStorageへの読み書きをセットで理解することが、実装の第一歩です。

localStorage / sessionStorage / Cookie の違いと最適解

JavaScriptでデータをブラウザに保存する方法は複数あります。お気に入り機能に最適なものを選ぶために、それぞれの特徴と違いをしっかり把握しておきましょう。

3つの保存方法の比較

比較項目localStoragesessionStorageCookie
データの保持期間明示的に削除するまで半永久的タブ・ブラウザを閉じると消える有効期限を設定可能
保存容量の目安約5MB約5MB約4KB
サーバーへの送信しないしないリクエストのたびに自動送信
JavaScriptからのアクセス容易容易やや複雑
主な用途設定・お気に入りなどの永続データ一時的なフォームデータなど認証トークン・セッション管理など

localStorageの詳細

localStorageは、ブラウザにキーと値のペアでデータを保存できる仕組みです。同じオリジン(プロトコル+ドメイン+ポートの組み合わせ)であれば、異なるページ間でもデータを共有できます。

つまり、https://example.com/products/ で保存したお気に入りデータを、https://example.com/mypage/ でも読み込むことができます。これはお気に入り機能にとって非常に重要な特性です。

// 基本的な使い方
localStorage.setItem('key', 'value');       // 保存
localStorage.getItem('key');                // 取得 → 'value'
localStorage.removeItem('key');             // 削除
localStorage.clear();                       // 全削除

保存できるのは文字列のみという点に注意が必要です。配列やオブジェクトをそのまま保存しようとすると、[object Object] のような文字列に変換されてしまいます。これを回避するために、後述するJSON形式への変換が必要になります。

sessionStorageの詳細

sessionStorageはlocalStorageと使い方はほぼ同じですが、データの保持期間がタブを閉じるまでに限られます。ブラウザのタブを閉じた瞬間にデータは消えるため、「セッション中だけ有効なカート」や「一時的な検索条件の保持」などに向いています。

お気に入り機能のように「次回アクセスしても残っていてほしいデータ」には不向きです。

Cookieの詳細

CookieはHTTPリクエストのたびにサーバーへ自動送信されるため、サーバーサイドとの連携が必要な場面で活躍します。しかし保存容量が約4KBと非常に小さいうえ、お気に入りアイテムのIDリストを都度サーバーに送信するのは無駄が多く、セキュリティ上のリスクにもなり得ます。JavaScriptだけで完結させるお気に入り機能には不向きです。

結論:お気に入り機能にはlocalStorageが最適解

  • データを半永久的に保持できる
  • 同一オリジン内であればどのページからでも読み書きできる
  • 容量が十分(数十〜数百件のアイテムIDであれば全く問題ない)
  • JavaScriptからシンプルに操作できる

これらの理由から、JavaScriptだけで作るお気に入り機能の保存先としては、localStorageが最もバランスのとれた選択肢です。

JavaScriptだけで作るお気に入り機能の全体構成

実装に入る前に、全体の設計図を頭に入れておきましょう。コードを書き始める前に構成を把握しておくことで、どこで何をすべきかが明確になり、実装ミスを大幅に減らせます。

ファイル構成

最小構成であれば、以下の3ファイルで実装できます。

project/
├── index.html        ← 商品一覧・記事一覧ページ
├── style.css         ← ボタンのデザイン(通常時・お気に入り済み時)
└── favorite.js       ← お気に入り機能のすべてのロジック

複数ページにまたがる場合も、favorite.js を共通で読み込む構成にすれば、ロジックを使い回せます。

JavaScriptで担う役割の全体像

favorite.js の中で行う処理は、大きく次の4つに分類されます。

① データの読み込み(初期化) ページが読み込まれたとき、localStorageからお気に入りリストを取得し、JavaScriptの配列として展開します。

// localStorageから取得 → なければ空配列
const favorites = JSON.parse(localStorage.getItem('favorites')) || [];

② ボタンへの初期状態の反映 取得したお気に入りリストをもとに、各ボタンの表示状態(通常 or お気に入り済み)を設定します。ページ遷移後に「お気に入り状態が消えた」と感じさせないために欠かせない処理です。

③ クリック処理(トグルロジック) ユーザーがボタンをクリックしたとき、対象アイテムのIDがお気に入りリストに存在するかを確認し、「追加」か「削除」かを判定して配列を更新します。その後、localStorageへ保存します。

④ UIへの反映 配列の更新に合わせて、ボタンのCSSクラスを付け外しし、視覚的なフィードバックをユーザーに与えます。

データ設計:何を保存するか

localStorageに保存するデータは、アイテムを一意に識別できるIDの配列にするのが最もシンプルで扱いやすい設計です。

// localStorageに保存するデータのイメージ
["item-001", "item-045", "item-123"]

商品名や画像URLなど多くの情報を一緒に保存するとデータ量が膨れ上がるため、IDだけを保存し、表示に必要な情報はHTMLから動的に取得するアプローチが現場でよく使われます。

これでお気に入り機能の基礎知識は十分に揃いました。次のセクションから、実際のコードを書いていきます。

HTML+CSS+JavaScriptで作る「お気に入りボタン」実装手順

HTMLのbuttonタグとdata-id属性で商品IDや記事IDを紐付けるマークアップ例

お気に入り機能を実装するうえで、HTMLのマークアップは非常に重要な出発点です。「どのアイテムのお気に入りボタンが押されたか」をJavaScript側で正確に識別するために、data属性を使ってアイテムIDをボタンに紐付けます。

data属性とは

data属性(data-*)は、HTML要素に任意のカスタムデータを埋め込むための仕組みです。JavaScriptから dataset プロパティを通じて簡単に取得できるため、「このボタンはどの商品に対応しているか」という情報をHTML上で管理するのに最適です。

商品カードのマークアップ例(ECサイト想定)

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>お気に入り機能サンプル</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>

  <div class="product-list">

    <!-- 商品カード 1 -->
    <div class="product-card">
      <img src="product-001.jpg" alt="商品A">
      <h2 class="product-name">商品A</h2>
      <p class="product-price">¥3,980</p>
      <button
        class="favorite-btn"
        data-id="item-001"
        aria-label="商品Aをお気に入りに追加"
        aria-pressed="false"
      >
        ♡ お気に入り
      </button>
    </div>

    <!-- 商品カード 2 -->
    <div class="product-card">
      <img src="product-002.jpg" alt="商品B">
      <h2 class="product-name">商品B</h2>
      <p class="product-price">¥5,480</p>
      <button
        class="favorite-btn"
        data-id="item-002"
        aria-label="商品Bをお気に入りに追加"
        aria-pressed="false"
      >
        ♡ お気に入り
      </button>
    </div>

    <!-- 商品カード 3 -->
    <div class="product-card">
      <img src="product-003.jpg" alt="商品C">
      <h2 class="product-name">商品C</h2>
      <p class="product-price">¥2,200</p>
      <button
        class="favorite-btn"
        data-id="item-003"
        aria-label="商品Cをお気に入りに追加"
        aria-pressed="false"
      >
        ♡ お気に入り
      </button>
    </div>

  </div>

  <script src="favorite.js"></script>
</body>
</html>

マークアップの各ポイント解説

data-id 属性でアイテムを識別する 各ボタンに data-id="item-001" のようにユニークなIDを付与しています。JavaScriptではこの値を button.dataset.id で取得します。IDの値は、CMSや動的生成であればサーバー側から出力する形にすれば実務でもそのまま使えます。

class="favorite-btn" で一括取得できるようにする すべてのお気に入りボタンに共通のクラス名を付けることで、querySelectorAll('.favorite-btn') でまとめて取得できます。

aria-labelaria-pressed でアクセシビリティを確保するaria-label はスクリーンリーダー向けにボタンの説明を提供します。aria-pressed はボタンのオン・オフ状態をアクセシビリティツリーに伝えるための属性で、お気に入り登録時に "true"、解除時に "false" へ切り替えます。視覚的なデザインだけでなく、こうしたアクセシビリティ対応も実務では重要です。

CSSでお気に入りボタンのデザインを作る

通常状態とお気に入り済み状態でボタンの見た目を変えるために、CSSで .is-favorite クラスのスタイルを定義しておきます。

/* style.css */

.product-list {
  display: flex;
  flex-wrap: wrap;
  gap: 24px;
  padding: 24px;
}

.product-card {
  width: 200px;
  border: 1px solid #e0e0e0;
  border-radius: 8px;
  padding: 16px;
  text-align: center;
}

/* お気に入りボタン:通常状態 */
.favorite-btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin-top: 12px;
  padding: 8px 16px;
  border: 2px solid #ccc;
  border-radius: 20px;
  background-color: #fff;
  color: #666;
  font-size: 14px;
  cursor: pointer;
  transition: all 0.2s ease;
}

.favorite-btn:hover {
  border-color: #e74c3c;
  color: #e74c3c;
}

/* お気に入りボタン:登録済み状態 */
.favorite-btn.is-favorite {
  border-color: #e74c3c;
  background-color: #e74c3c;
  color: #fff;
}

.favorite-btn.is-favorite:hover {
  background-color: #c0392b;
  border-color: #c0392b;
}

JavaScriptから .is-favorite クラスを付け外しするだけで、ボタンの見た目が自動的に切り替わります。スタイルの管理はCSSに、ロジックの管理はJavaScriptに、と責務を分離するのがメンテナンスしやすいコードの基本です。

addEventListenerとquerySelectorでお気に入りボタンのクリック処理を書く方法

HTMLとCSSが用意できたら、次はJavaScriptでクリック処理を実装します。ここでは addEventListenerquerySelector を使った、実務でも通用するコードの書き方を解説します。

基本的なイベント登録の流れ

クリック処理を実装するには、まずボタン要素を取得し、そこにクリックイベントのリスナーを登録します。

// 1つのボタンに対してイベントを登録する基本形
const btn = document.querySelector('.favorite-btn');
btn.addEventListener('click', function() {
  console.log('クリックされました');
});

しかしお気に入りボタンはページ上に複数存在するため、querySelector(1件取得)ではなく querySelectorAll(全件取得)を使い、取得したNodeListをループ処理してそれぞれにイベントを登録します。

複数ボタンへのイベント登録

// favorite.js

// すべてのお気に入りボタンを取得
const favoriteButtons = document.querySelectorAll('.favorite-btn');

// 各ボタンにクリックイベントを登録
favoriteButtons.forEach(function(button) {
  button.addEventListener('click', function() {
    // クリックされたボタンのdata-id属性を取得
    const itemId = button.dataset.id;
    console.log('クリックされたアイテムID:', itemId);
  });
});

button.dataset.id は、HTML上の data-id="item-001" という属性値を取得するプロパティです。data- に続く部分(ここでは id)がそのままプロパティ名になります。

thisキーワードとアロー関数の注意点

イベントリスナー内で this を使う場合、アロー関数と通常の function で挙動が異なります。アロー関数では this が外側のスコープを参照するため、クリックされたボタン自身を参照したい場合は通常の function を使うか、引数 event.currentTarget を使います。

// 方法①:通常のfunction(thisがクリックされたボタンを指す)
button.addEventListener('click', function() {
  console.log(this.dataset.id); // クリックしたボタンのIDが取得できる
});

// 方法②:アロー関数(buttonをそのまま使う)
button.addEventListener('click', () => {
  console.log(button.dataset.id); // こちらも正しく動作する
});

// 方法③:eventオブジェクトを使う(最も明示的)
button.addEventListener('click', (event) => {
  const clickedButton = event.currentTarget;
  console.log(clickedButton.dataset.id);
});

実務では方法②か方法③が読みやすくバグが少ないためよく使われます。

イベント委譲(Event Delegation)を使った効率的な実装

商品が動的に追加されるケース(無限スクロールやAjaxで商品を追加する場合など)では、querySelectorAll でループしてイベントを登録する方法では、後から追加された要素にはイベントが登録されません。

この問題を解決するのがイベント委譲です。親要素にイベントリスナーをひとつだけ登録し、クリックが発生した子要素を event.target で特定する方法です。

// 親要素にイベントリスナーを1つだけ登録する(イベント委譲)
const productList = document.querySelector('.product-list');

productList.addEventListener('click', function(event) {
  // クリックされた要素がお気に入りボタンかどうかを確認
  const button = event.target.closest('.favorite-btn');
  if (!button) return; // ボタン以外がクリックされた場合は何もしない

  const itemId = button.dataset.id;
  console.log('クリックされたアイテムID:', itemId);
});

closest() メソッドは、クリックされた要素自身またはその祖先要素の中から、指定したセレクタに一致する最も近い要素を返します。ボタン内のアイコンやテキストがクリックされた場合でも、正しくボタン要素を取得できるため、実務ではこの書き方を強く推奨します。

登録済みアイテムの判定ロジックと重複登録を防ぐ方法

クリックを検知したあと、「そのアイテムはすでにお気に入り登録済みか?」を判定し、登録・解除のどちらを行うかを決定するのがトグルロジックの核心です。

配列の includes() で登録済みを判定する

お気に入りアイテムのIDを配列で管理している場合、Array.prototype.includes() を使って登録済みかどうかを確認できます。

const favorites = ['item-001', 'item-003']; // 現在のお気に入りリスト

// item-001 はお気に入り済みか?
console.log(favorites.includes('item-001')); // → true
console.log(favorites.includes('item-002')); // → false

重複登録を防ぐトグルロジックの実装

/**
 * お気に入りのトグル処理
 * @param {string} itemId - 対象アイテムのID
 * @param {Array} favorites - 現在のお気に入りリスト(配列)
 * @returns {Array} - 更新後のお気に入りリスト
 */
function toggleFavorite(itemId, favorites) {
  const index = favorites.indexOf(itemId);

  if (index === -1) {
    // リストに存在しない → 追加
    favorites.push(itemId);
    console.log(`${itemId} をお気に入りに追加しました`);
  } else {
    // リストに存在する → 削除(重複登録の防止にもなる)
    favorites.splice(index, 1);
    console.log(`${itemId} をお気に入りから削除しました`);
  }

  return favorites;
}

indexOf() は指定した値が配列内に存在すればそのインデックス(0以上の整数)を、存在しなければ -1 を返します。-1 かどうかで追加・削除を分岐させるのが定番のパターンです。

Set を使った重複防止(より堅牢な方法)

JavaScriptの Set は重複する値を持てないデータ構造です。配列の代わりに Set を使うことで、重複登録を構造的に防ぐことができます。

// Setを使ったお気に入り管理
let favoritesSet = new Set(['item-001', 'item-003']);

function toggleFavoriteWithSet(itemId) {
  if (favoritesSet.has(itemId)) {
    favoritesSet.delete(itemId); // 削除
  } else {
    favoritesSet.add(itemId);    // 追加(重複は自動的に無視される)
  }

  // localStorageへの保存にはArrayに変換する必要がある
  const favoritesArray = Array.from(favoritesSet);
  localStorage.setItem('favorites', JSON.stringify(favoritesArray));
}

ただし Set はlocalStorageに直接保存できないため、保存時には Array.from() で配列に変換する必要があります。読み込み時も配列から Set へ変換するひと手間が加わります。シンプルさを優先するなら配列、重複防止を構造的に保証したいなら Set と使い分けましょう。

ボタンのUIに反映するコードとの統合

判定ロジックとUIの反映をまとめた実装例は次のとおりです。

// favorite.js(クリック処理からUI反映までの一連の流れ)

// localStorageからお気に入りリストを取得(なければ空配列)
let favorites = JSON.parse(localStorage.getItem('favorites')) || [];

const productList = document.querySelector('.product-list');

productList.addEventListener('click', function(event) {
  const button = event.target.closest('.favorite-btn');
  if (!button) return;

  const itemId = button.dataset.id;
  const index = favorites.indexOf(itemId);

  if (index === -1) {
    // 未登録 → 追加
    favorites.push(itemId);
    button.classList.add('is-favorite');
    button.textContent = '♥ お気に入り済み';
    button.setAttribute('aria-pressed', 'true');
  } else {
    // 登録済み → 削除
    favorites.splice(index, 1);
    button.classList.remove('is-favorite');
    button.textContent = '♡ お気に入り';
    button.setAttribute('aria-pressed', 'false');
  }

  // 更新したリストをlocalStorageに保存
  localStorage.setItem('favorites', JSON.stringify(favorites));
});

実際の表示

See the Pen js-favorite-button-01 by watashi-xyz (@watashi-xyz) on CodePen.

このコードで、クリックのたびに「判定 → 配列の更新 → localStorage保存 → UI反映」という一連の処理が完結します。次のセクションでは、localStorage周りの処理をさらに詳しく解説します。

LocalStorageを使ってお気に入り状態を保存する方法

配列でお気に入りデータを管理する設計

localStorageへの読み書きを実装する前に、「どのようなデータ構造でお気に入り情報を管理するか」を設計しておくことが重要です。設計が曖昧なまま実装を進めると、後からバグの温床になったり、機能追加のたびにコードを大幅に書き直す羽目になります。

シンプルな設計:IDの配列

最もシンプルで実用的な設計は、お気に入りに登録されたアイテムのIDだけを配列で管理する方法です。

// お気に入りリストのデータ構造(IDの配列)
const favorites = ['item-001', 'item-045', 'item-123'];

この設計のメリットは次のとおりです。

  • データ量が最小限で済む(localStorageの容量を無駄に消費しない)
  • 追加・削除・存在確認などの操作がシンプルなコードで書ける
  • IDさえあれば、表示に必要な情報(商品名・画像・価格など)はHTMLやAPIから取得できる

ECサイトや記事一覧のように、アイテムの情報がすでにHTMLに存在している場面では、IDの配列だけを保存するこの設計が最も扱いやすいです。

拡張設計:オブジェクトの配列

将来的に「お気に入り一覧ページ」を作る予定がある場合や、localStorageだけで完結させたい場合は、IDだけでなく表示に必要な最小限の情報もあわせて保存するオブジェクト配列の設計が有効です。

// お気に入りリストのデータ構造(オブジェクトの配列)
const favorites = [
  {
    id: 'item-001',
    name: '商品A',
    price: 3980,
    image: 'product-001.jpg'
  },
  {
    id: 'item-045',
    name: '商品B',
    price: 5480,
    image: 'product-002.jpg'
  }
];

この設計はデータ量が増えるというデメリットがありますが、お気に入り一覧ページでHTMLを再構築する際に、localStorageのデータだけで表示を完結させられる点が強みです。

どちらの設計を選ぶべきか

観点IDの配列オブジェクトの配列
データ量少ない多い
実装のシンプルさ
お気に入り一覧ページの作りやすさ△(HTMLが必要)
データの鮮度(商品名・価格の変更への追従)◎(常に最新のHTMLを参照)△(保存時の情報が古くなる可能性あり)

本記事では、最も汎用性が高く初心者にも扱いやすいIDの配列を基本設計として採用します。この設計を前提に、以降のコードを解説していきます。

配列操作の基本メソッドを整理する

お気に入り機能で頻繁に使う配列操作メソッドをここで整理しておきましょう。

const favorites = ['item-001', 'item-045', 'item-123'];

// 要素の追加
favorites.push('item-200');
// → ['item-001', 'item-045', 'item-123', 'item-200']

// 要素の存在確認
favorites.includes('item-045');  // → true
favorites.includes('item-999');  // → false

// インデックスの取得(存在しない場合は -1)
favorites.indexOf('item-123');   // → 2
favorites.indexOf('item-999');   // → -1

// 要素の削除(インデックスを指定して1件削除)
const index = favorites.indexOf('item-045');
if (index !== -1) {
  favorites.splice(index, 1);
}
// → ['item-001', 'item-123', 'item-200']

// filterを使った削除(元の配列を変更せず新しい配列を返す)
const updated = favorites.filter(id => id !== 'item-001');
// → ['item-123', 'item-200']

splice は元の配列を直接変更します。一方 filter は新しい配列を返し元の配列は変更しません。どちらも実務で使われますが、意図しない副作用を避けたい場面では filter の方が安全です。


JSON.stringify / JSON.parseでLocalStorageに保存・読み込みする方法

localStorageが扱えるデータ型は文字列のみです。JavaScriptの配列やオブジェクトをそのまま setItem で保存しようとすると、自動的に文字列へ変換されますが、その結果は意図しないものになります。

配列をそのまま保存するとどうなるか

const favorites = ['item-001', 'item-045'];
localStorage.setItem('favorites', favorites);

// 取得してみると…
console.log(localStorage.getItem('favorites'));
// → "item-001,item-045"  ←カンマ区切りの文字列になってしまう

// これをそのまま使おうとすると…
const loaded = localStorage.getItem('favorites');
console.log(loaded.includes('item-001')); // → true(たまたま動く)
console.log(loaded.length);              // → 20(文字列の文字数が返る!)

配列として扱えなくなるため、pushsplice などの配列メソッドが使えなくなります。これは非常に発見しにくいバグの原因になります。

JSON.stringifyで配列を文字列に変換して保存する

この問題を解決するのが JSON.stringify() です。JavaScriptの配列やオブジェクトをJSON形式の文字列に変換し、localStorageに保存します。

const favorites = ['item-001', 'item-045', 'item-123'];

// 配列をJSON文字列に変換して保存
const jsonString = JSON.stringify(favorites);
console.log(jsonString); // → '["item-001","item-045","item-123"]'

localStorage.setItem('favorites', jsonString);

JSON.stringify() を通すことで、配列の構造を保ったまま文字列として保存できます。

JSON.parseで文字列を配列に戻して読み込む

保存したJSON文字列を配列として使うには、JSON.parse() で元のデータ構造に戻す必要があります。

// localStorageからJSON文字列を取得
const jsonString = localStorage.getItem('favorites');
console.log(jsonString); // → '["item-001","item-045","item-123"]'

// JSON文字列を配列に変換
const favorites = JSON.parse(jsonString);
console.log(favorites);        // → ['item-001', 'item-045', 'item-123']
console.log(Array.isArray(favorites)); // → true(正しく配列になっている)
console.log(favorites.length); // → 3(配列の要素数が返る)

保存・読み込みをまとめた関数として管理する

保存と読み込みの処理を毎回べた書きするのではなく、関数化しておくと再利用性が高まり、修正箇所も一元化できます。

// お気に入りリストをlocalStorageから読み込む
function loadFavorites() {
  const json = localStorage.getItem('favorites');
  return JSON.parse(json) || [];
}

// お気に入りリストをlocalStorageに保存する
function saveFavorites(favorites) {
  localStorage.setItem('favorites', JSON.stringify(favorites));
}

JSON.parse(json) || [] という書き方は非常に重要です。localStorageに ‘favorites’ というキーが存在しない場合、getItemnull を返します。JSON.parse(null)null を返すため、|| [] で空配列にフォールバックさせることで、初回アクセス時のエラーを防ぎます。

オブジェクトの配列も同様に扱える

オブジェクトの配列を保存・読み込みする場合も、まったく同じ方法が使えます。

const favorites = [
  { id: 'item-001', name: '商品A', price: 3980 },
  { id: 'item-045', name: '商品B', price: 5480 }
];

// 保存
localStorage.setItem('favorites', JSON.stringify(favorites));

// 読み込み
const loaded = JSON.parse(localStorage.getItem('favorites')) || [];
console.log(loaded[0].name); // → '商品A'

JSON形式はネストした構造も正確に変換できるため、配列・オブジェクトを問わず安心して使えます。

JSON変換で注意すべきデータ型

JSON.stringify はすべてのJavaScriptの値を正確に変換できるわけではありません。次のデータ型は変換時に注意が必要です。

// undefinedは変換されず消える
JSON.stringify({ a: undefined }); // → '{}'

// 関数も変換されず消える
JSON.stringify({ fn: function() {} }); // → '{}'

// DateオブジェクトはISO文字列になる
JSON.stringify(new Date()); // → '"2025-01-01T00:00:00.000Z"'
// JSON.parseで戻してもDateオブジェクトには戻らない(文字列のまま)

// NaNはnullに変換される
JSON.stringify(NaN); // → 'null'

お気に入り機能で保存するのは文字列のIDや数値・文字列のみのシンプルなデータであれば、これらの問題に遭遇することはほぼありません。ただし将来的に複雑なデータを扱う場合は意識しておきましょう。


ページ読み込み時にLocalStorageを読み込みお気に入り状態を復元する方法

お気に入り機能において「ページ遷移後も状態が保たれている」という体験は、ユーザーにとって非常に重要です。ページを再読み込みしたときや別ページから戻ってきたときに、登録したはずのお気に入りが解除されているように見えるのは、localStorageへの保存はできていても、読み込み時の状態復元処理が抜けていることが原因です。

状態復元の考え方

ページが読み込まれるたびに、次の処理を実行する必要があります。

ページ読み込み
    ↓
localStorageからお気に入りリストを取得(JSON.parse)
    ↓
ページ上のすべてのお気に入りボタンをループ
    ↓
各ボタンのdata-idがお気に入りリストに含まれるか確認
    ↓
含まれる → .is-favoriteクラスを付与・aria-pressedをtrueに
含まれない → 何もしない(デフォルト状態のまま)

状態復元の実装

/**
 * ページ読み込み時にお気に入り状態をボタンに反映する
 */
function restoreFavoriteState() {
  const favorites = loadFavorites(); // localStorageから配列を取得
  const buttons = document.querySelectorAll('.favorite-btn');

  buttons.forEach(function(button) {
    const itemId = button.dataset.id;

    if (favorites.includes(itemId)) {
      // お気に入り登録済みのボタンにクラスを付与
      button.classList.add('is-favorite');
      button.textContent = '♥ お気に入り済み';
      button.setAttribute('aria-pressed', 'true');
    }
  });
}

DOMContentLoadedイベントで確実に実行する

この復元処理は、HTML要素(ボタン)がすべてレンダリングされた後に実行する必要があります。<script> タグを </body> の直前に置いている場合は問題ありませんが、<head> 内に置く場合や、確実性を高めたい場合は DOMContentLoaded イベントを使いましょう。

document.addEventListener('DOMContentLoaded', function() {
  restoreFavoriteState();
});

DOMContentLoaded はHTMLの解析が完了し、DOMが構築された時点で発火します。画像などの外部リソースの読み込みを待たないため、window.onload よりも早く実行されます。

すべてをまとめた完成版コード

ここまでの実装をひとつのファイルにまとめた、コピペで動く完成版コードです。

// favorite.js - 完成版

// --- ユーティリティ関数 ---

/**
 * localStorageからお気に入りリストを読み込む
 * @returns {Array<string>} お気に入りアイテムIDの配列
 */
function loadFavorites() {
  const json = localStorage.getItem('favorites');
  return JSON.parse(json) || [];
}

/**
 * お気に入りリストをlocalStorageに保存する
 * @param {Array<string>} favorites - 保存するお気に入りリスト
 */
function saveFavorites(favorites) {
  localStorage.setItem('favorites', JSON.stringify(favorites));
}

/**
 * ボタンの表示をお気に入り状態に応じて更新する
 * @param {HTMLElement} button - 対象のボタン要素
 * @param {boolean} isFavorite - お気に入り登録済みかどうか
 */
function updateButtonUI(button, isFavorite) {
  if (isFavorite) {
    button.classList.add('is-favorite');
    button.textContent = '♥ お気に入り済み';
    button.setAttribute('aria-pressed', 'true');
  } else {
    button.classList.remove('is-favorite');
    button.textContent = '♡ お気に入り';
    button.setAttribute('aria-pressed', 'false');
  }
}

// --- 初期化処理 ---

/**
 * ページ読み込み時にお気に入り状態を復元する
 */
function restoreFavoriteState() {
  const favorites = loadFavorites();
  const buttons = document.querySelectorAll('.favorite-btn');

  buttons.forEach(function(button) {
    const isFavorite = favorites.includes(button.dataset.id);
    updateButtonUI(button, isFavorite);
  });
}

// --- イベント登録 ---

/**
 * お気に入りボタンのクリックイベントを登録する
 */
function initFavoriteButtons() {
  const productList = document.querySelector('.product-list');
  if (!productList) return; // 要素が存在しない場合は処理しない

  productList.addEventListener('click', function(event) {
    const button = event.target.closest('.favorite-btn');
    if (!button) return;

    const itemId = button.dataset.id;
    const favorites = loadFavorites();
    const index = favorites.indexOf(itemId);

    if (index === -1) {
      favorites.push(itemId);         // 未登録 → 追加
    } else {
      favorites.splice(index, 1);     // 登録済み → 削除
    }

    saveFavorites(favorites);          // localStorageに保存
    updateButtonUI(button, index === -1); // UIを更新
  });
}

// --- エントリーポイント ---

document.addEventListener('DOMContentLoaded', function() {
  restoreFavoriteState();   // お気に入り状態を復元
  initFavoriteButtons();    // クリックイベントを登録
});

このコードの設計上のポイント

  • 関数の役割を明確に分離している:読み込み・保存・UI更新・イベント登録・初期化、それぞれを独立した関数として定義しています。どこかを修正したいときに、該当する関数だけを見ればよいためメンテナンス性が高まります。
  • loadFavoritessaveFavorites を一元化している:localStorageへのアクセスをこの2つの関数に集約することで、キー名の変更やデータ構造の変更があっても修正箇所が最小限で済みます。
  • updateButtonUI を共通化している:状態復元時とクリック時の両方で同じ関数を呼び出すことで、「復元時だけボタンのテキストが変わらない」といった不整合を防ぎます。
  • if (!productList) return :対象の親要素が存在しないページでスクリプトが読み込まれた場合に、エラーで止まらないようにする防御的なコードです。複数ページで同じJSファイルを共通読み込みしている場合に特に重要です。

この完成版コードをベースに、次のセクションで紹介するエラー対処を組み合わせることで、実務レベルのお気に入り機能が完成します。

よくあるエラーとバグの回避方法

localStorageがnullになる問題の対処法

localStorageを使った実装で最も頻繁に遭遇するのが、getItemnull を返すケースです。この問題を正しく理解して対処しないと、JSON.parse(null)null.includes() といった処理でエラーが発生し、お気に入り機能全体が動作しなくなります。

nullが返される主な原因

localStorage.getItem('favorites')null を返すのは、主に次の3つの状況です。

  1. 初回アクセス時:まだ一度もお気に入り登録をしていないため、localStorageに ‘favorites’ キーが存在しない
  2. ユーザーがlocalStorageを手動でクリアした後:ブラウザの開発者ツールやブラウザ設定から削除された場合
  3. プライベートブラウジングモード:一部のブラウザではlocalStorageが制限されており、セッション終了と同時に削除される

nullに対する防御的なコード

最も基本的な対処は、JSON.parse の結果が null だった場合に空配列へフォールバックさせることです。

// NG:nullのままJSON.parseするとnullが返り、後続処理でエラーになる
const favorites = JSON.parse(localStorage.getItem('favorites'));
favorites.includes('item-001'); // → TypeError: Cannot read properties of null

// OK:nullの場合は空配列にフォールバックする
const favorites = JSON.parse(localStorage.getItem('favorites')) || [];
favorites.includes('item-001'); // → false(エラーにならない)

|| [] によるフォールバックは、JSON.parsenull または undefined を返した場合に空配列を代入するシンプルかつ効果的な書き方です。前述の loadFavorites 関数でこの処理を一元化しているため、呼び出しのたびに書き忘れる心配がありません。

localStorageそのものが使えない環境への対処

プライベートブラウジングモードや、ブラウザの設定でlocalStorageへのアクセスが完全にブロックされている環境では、localStorage.setItem を呼び出した時点で SecurityError が発生します。本番環境で動作させるなら、この例外も捕捉しておくのが堅牢な実装です。

/**
 * localStorageが使用可能かどうかを確認する
 * @returns {boolean}
 */
function isLocalStorageAvailable() {
  try {
    const testKey = '__storage_test__';
    localStorage.setItem(testKey, '1');
    localStorage.removeItem(testKey);
    return true;
  } catch (e) {
    return false;
  }
}

/**
 * localStorageが使えない場合はインメモリ配列で代替する
 */
let inMemoryFavorites = [];

function loadFavorites() {
  if (!isLocalStorageAvailable()) {
    return inMemoryFavorites;
  }
  return JSON.parse(localStorage.getItem('favorites')) || [];
}

function saveFavorites(favorites) {
  if (!isLocalStorageAvailable()) {
    inMemoryFavorites = favorites; // メモリ上にのみ保持
    return;
  }
  localStorage.setItem('favorites', JSON.stringify(favorites));
}

インメモリでの代替はページを閉じると消えますが、localStorageが使えない環境でも機能が壊れずに動作し続けるという点で、ユーザー体験を損ないません。

JSON.parseで想定外のデータが渡された場合のエラー

localStorageに手動で不正な文字列が書き込まれていた場合、JSON.parseSyntaxError を投げます。これも try...catch で対処しておくと安全です。

function loadFavorites() {
  try {
    const json = localStorage.getItem('favorites');
    const parsed = JSON.parse(json);
    // parseの結果が配列でない場合(nullやオブジェクトなど)も空配列に
    return Array.isArray(parsed) ? parsed : [];
  } catch (e) {
    // JSON.parseが失敗した場合(不正なJSON文字列など)
    console.warn('お気に入りデータの読み込みに失敗しました。リセットします。', e);
    localStorage.removeItem('favorites'); // 不正データを削除
    return [];
  }
}

Array.isArray() によるチェックを加えることで、JSON.parse の結果が配列以外(null・オブジェクト・数値など)だった場合にも安全に空配列へフォールバックできます。


ページ遷移後にお気に入り状態が反映されない原因と対処法

「お気に入りボタンを押したのに、別ページに移動して戻ってきたら元に戻っていた」——これはlocalStorageを使った実装でよく報告される問題です。多くの場合、原因はlocalStorageの保存に失敗しているのではなく、ページ読み込み時の状態復元処理が実装されていない、または正しく動いていないことにあります。

原因① 状態復元処理が実装されていない

ページを遷移すると、JavaScriptのメモリ(変数)はすべてリセットされます。localStorageに保存した内容は残っていますが、ボタンのCSSクラスや表示テキストはHTMLのデフォルト状態に戻ります。そのため、ページ読み込みのたびに「localStorageを読み込んでボタンの状態を復元する」処理が必ず必要です。

// ページ読み込み時に必ずこの関数を呼ぶ
function restoreFavoriteState() {
  const favorites = loadFavorites();
  const buttons = document.querySelectorAll('.favorite-btn');

  buttons.forEach(function(button) {
    const isFavorite = favorites.includes(button.dataset.id);
    updateButtonUI(button, isFavorite);
  });
}

document.addEventListener('DOMContentLoaded', restoreFavoriteState);

この処理が DOMContentLoaded 内に書かれていない場合、ボタン要素がDOMに存在する前にJavaScriptが実行され、querySelectorAll が空のNodeListを返してしまいます。

原因② スクリプトの読み込み順序の問題

<script src="favorite.js"><head> 内に置いている場合、HTMLの解析が完了する前にスクリプトが実行されるため、ボタン要素がまだ存在しません。

<!-- NG:<head>内に置くと、ボタンが存在する前にスクリプトが実行される -->
<head>
  <script src="favorite.js"></script>
</head>

<!-- OK①:</body>の直前に置く(推奨) -->
<body>
  ...
  <script src="favorite.js"></script>
</body>

<!-- OK②:defer属性を使う(<head>内でもDOMの準備完了後に実行される) -->
<head>
  <script src="favorite.js" defer></script>
</head>

defer 属性を使うと、スクリプトのダウンロードはHTMLの解析と並行して行われ、DOMの構築が完了した後に実行されます。DOMContentLoaded と組み合わせた場合と実質同等の動作になりますが、両方使っても問題ありません。

原因③ 複数ページで異なるキー名を使っている

localStorageはキーと値のペアで管理されます。保存時のキー名と読み込み時のキー名が異なると、正しくデータを取得できません。

// ページAで保存(キー名: 'favorites')
localStorage.setItem('favorites', JSON.stringify(['item-001']));

// ページBで読み込み(キー名が違う!)
localStorage.getItem('favorite'); // → null(取得できない)

複数ページにわたってお気に入り機能を使う場合は、キー名を定数として管理するのが安全です。

// キー名を定数化して一元管理
const STORAGE_KEY = 'favorites';

function loadFavorites() {
  return JSON.parse(localStorage.getItem(STORAGE_KEY)) || [];
}

function saveFavorites(favorites) {
  localStorage.setItem(STORAGE_KEY, JSON.stringify(favorites));
}

定数として定義しておけば、将来キー名を変更する際も1箇所の修正で済みます。

原因④ 同一ページ内でのSPA的な動的更新への対応

ReactやVue.jsを使わず純粋なJavaScriptでSPA的な画面切り替えをしている場合、DOMContentLoaded は最初の1回しか発火しません。動的に商品リストを再描画するたびに restoreFavoriteState() を手動で呼び出す必要があります。

// 商品リストを動的に再描画する関数
function renderProducts(products) {
  const list = document.querySelector('.product-list');
  list.innerHTML = products.map(p => `
    <div class="product-card">
      <h2>${p.name}</h2>
      <button class="favorite-btn" data-id="${p.id}">♡ お気に入り</button>
    </div>
  `).join('');

  // 再描画後に必ず状態を復元する
  restoreFavoriteState();
}

Cookie削除やブラウザ設定でデータが消える場合の注意点

localStorageは便利なデータ保存手段ですが、ユーザーの操作やブラウザの設定によってデータが消えるリスクがあります。この特性を理解したうえで、適切なフォールバックとユーザーへの案内を実装しておくことが実務では重要です。

localStorageのデータが消えるケース

localStorageのデータが失われる主な原因を整理します。

原因詳細
ブラウザのキャッシュ・履歴削除「閲覧データを削除」でサイトデータを消去するとlocalStorageも削除される
ブラウザの設定による自動削除「終了時にCookieとサイトデータを削除する」設定が有効な場合
プライベートブラウジングセッション終了と同時にlocalStorageが削除される(Chromeのシークレットモード等)
ストレージ容量の上限超過約5MBの上限を超えると QuotaExceededError が発生する
別のオリジンからのアクセスhttp://https:// は別オリジンとして扱われるためデータを共有できない

「CookieとサイトデータをChromeで削除する」とlocalStorageも消える

多くのユーザーは「Cookieを削除する」という操作でlocalStorageまで消えるとは思っていません。しかしChromeの「閲覧データを削除」画面で「Cookieと他のサイトデータ」にチェックを入れると、localStorageのデータも同時に削除されます。

Chromeでデータが消える操作:
設定 → プライバシーとセキュリティ → 閲覧履歴データの削除
→「Cookieと他のサイトデータ」にチェック → データを削除

この仕様はユーザーにとって直感的でないため、重要なデータをlocalStorageだけに頼ることのリスクを認識しておく必要があります。

プライベートブラウジングモードの挙動

ChromeのシークレットモードやSafariのプライベートブラウズでは、localStorageへの書き込みはセッション中は有効ですが、タブを閉じた時点でデータが消えます。また、通常モードのlocalStorageとは完全に分離されているため、通常モードで登録したお気に入りはシークレットモードでは参照できません。

httpとhttpsでデータが共有されない問題

localStorageはオリジン(プロトコル+ドメイン+ポート)ごとに管理されます。本番環境が https://example.com でも、開発中に http://localhost で動作確認をしている場合、localStorageの内容は完全に別管理です。

<http://example.com>  → localStorageのデータは独立
<https://example.com> → localStorageのデータは独立(上記とは別)
<http://localhost>    → localStorageのデータは独立(上記2つとは別)

本番でテストしたデータが開発環境で再現しないという混乱の原因になるため、開発段階からブラウザの開発者ツールでlocalStorageの中身を確認する習慣をつけておきましょう。

ストレージ容量の上限超過への対処

localStorageの容量上限は概ね5MBです。アイテムIDだけを保存するシンプルな設計であれば、数万件のIDを保存しても数百KBにしかならず、通常は問題になりません。しかし画像URLや長い文字列を大量に保存する設計の場合は上限に達する可能性があります。

function saveFavorites(favorites) {
  try {
    localStorage.setItem(STORAGE_KEY, JSON.stringify(favorites));
  } catch (e) {
    if (e.name === 'QuotaExceededError') {
      // 容量上限に達した場合の処理
      console.warn('localStorageの容量が上限に達しました。古いデータを削除してください。');
      // 必要であれば古いデータを削除するロジックを追加
    } else {
      console.error('localStorageへの保存に失敗しました:', e);
    }
  }
}

データが消えることを前提とした設計の考え方

localStorageのデータはあくまで「消える可能性のある一時的なブラウザデータ」として扱うことが重要です。ビジネス的に重要なお気に入りデータ(会員登録と連携するお気に入りなど)は、サーバーサイドのデータベースに保存することが正しいアーキテクチャです。

JavaScriptのみのlocalStorage実装は、次のような用途に留めるのが現実的です。

  • ログイン不要の軽量なブックマーク機能(「後で読む」「比較リスト」など)
  • プロトタイプやデモ用途でサーバーなしに動作確認したい場合
  • サーバー連携のバックアップとして、通信失敗時のキャッシュとして使う場合

もしサーバーサイドとの連携が可能な環境であれば、ページ読み込み時にサーバーのデータを正とし、localStorageはキャッシュとして使うという設計が、最もユーザー体験とデータ整合性のバランスが取れたアプローチです。

よくある質問(FAQ)

Q1. localStorageに保存したお気に入りは、ブラウザを閉じても残りますか?

はい、残ります。localStorageはブラウザを閉じてもデータが削除されず、明示的に localStorage.removeItem()localStorage.clear() を実行するか、ユーザーがブラウザの閲覧データを削除しない限り、半永久的に保持されます。ただし、プライベートブラウジング(シークレットモード)を使用している場合はタブを閉じると同時に削除されます。また、ブラウザの「Cookieとサイトデータを削除する」操作でも消えるため、重要なデータはサーバー側でも管理することを推奨します。


Q2. スマートフォンのブラウザでもlocalStorageは動作しますか?

はい、動作します。iOS Safari・Android Chrome・Firefox for Androidなど、現在主要なモバイルブラウザはすべてlocalStorageをサポートしています。ただし、iOSのSafariではプライベートブラウズモード時にlocalStorageへの書き込みが制限される場合があります。また、ストレージ容量はデスクトップと同様に概ね5MB程度ですが、デバイスの空き容量が極端に少ない場合は書き込みが失敗することがあります。前述の isLocalStorageAvailable() 関数で使用可否を確認し、使えない場合でもエラーにならない実装を心がけましょう。


Q3. お気に入り登録数の上限は設けるべきですか?

ケースによりますが、UX・パフォーマンスの観点から上限を設けることを推奨します。IDのみを保存する設計であれば、数百件程度ではlocalStorageの容量制限(約5MB)に達することはありません。しかし、ユーザーにとって「数百件のお気に入りリスト」は管理しづらくなります。実務では50〜200件程度を上限として、超えた場合にトーストメッセージや警告を表示するのが一般的です。

const MAX_FAVORITES = 100;

function addFavorite(itemId) {
  const favorites = loadFavorites();

  if (favorites.length >= MAX_FAVORITES) {
    alert(`お気に入りは最大${MAX_FAVORITES}件まで登録できます。`);
    return false;
  }

  if (!favorites.includes(itemId)) {
    favorites.push(itemId);
    saveFavorites(favorites);
  }
  return true;
}

Q4. お気に入りの件数をバッジやカウンターで表示するにはどうすればいいですか?

localStorageから取得した配列の length プロパティを使えば、簡単に実現できます。

<!-- ヘッダーにお気に入りカウンターを表示する例 -->
<div class="header-favorite">
  <a href="/favorites">
    ♡ お気に入り
    <span class="favorite-count">0</span>
  </a>
</div>
// カウンターを更新する関数
function updateFavoriteCount() {
  const favorites = loadFavorites();
  const countEl = document.querySelector('.favorite-count');
  if (!countEl) return;

  countEl.textContent = favorites.length;
  // 0件のときはバッジを非表示にする
  countEl.style.display = favorites.length > 0 ? 'inline' : 'none';
}

// ページ読み込み時とお気に入り変更時に呼び出す
document.addEventListener('DOMContentLoaded', updateFavoriteCount);

お気に入りを追加・削除するたびに updateFavoriteCount() を呼び出すことで、リアルタイムにカウンターが更新されます。


Q5. お気に入り一覧ページを別ページとして作るにはどうすればいいですか?

localStorageからIDリストを読み込み、対応するHTML要素を動的に生成する方法が基本です。IDだけを保存している設計の場合、商品情報(名前・価格・画像)は別途取得する必要があります。静的なHTMLサイトであれば、全商品データをJavaScriptのオブジェクトとして定義しておき、IDをキーに検索する方法がシンプルです。

// 全商品データをオブジェクトで管理(静的サイトの場合)
const allProducts = {
  'item-001': { name: '商品A', price: 3980, image: 'product-001.jpg' },
  'item-002': { name: '商品B', price: 5480, image: 'product-002.jpg' },
  'item-003': { name: '商品C', price: 2200, image: 'product-003.jpg' },
};

// お気に入り一覧ページの描画処理
function renderFavoritePage() {
  const favorites = loadFavorites();
  const container = document.querySelector('.favorites-list');

  if (favorites.length === 0) {
    container.innerHTML = '<p>お気に入りに登録された商品はありません。</p>';
    return;
  }

  container.innerHTML = favorites.map(function(id) {
    const product = allProducts[id];
    if (!product) return ''; // 存在しない商品IDはスキップ
    return `
      <div class="product-card">
        <img src="${product.image}" alt="${product.name}">
        <h2>${product.name}</h2>
        <p>¥${product.price.toLocaleString()}</p>
        <button class="favorite-btn is-favorite" data-id="${id}">
          ♥ お気に入り済み
        </button>
      </div>
    `;
  }).join('');
}

document.addEventListener('DOMContentLoaded', renderFavoritePage);

Q6. JavaScriptのお気に入り機能をWordPressやShopifyに組み込めますか?

はい、組み込めます。どちらもHTMLにカスタムJavaScriptを追加できるため、本記事のコードをそのまま活用できます。

WordPressの場合functions.phpwp_enqueue_script() でJSファイルを登録し、各投稿・商品テンプレート(single.phparchive.php など)のループ内で data-id="<?php the_ID(); ?>" のようにしてWordPressの投稿IDをdata属性に出力します。

Shopifyの場合theme.liquid または各セクションの .liquid ファイルに <script> タグを追加し、data-id="{{ product.id }}" でShopifyの商品IDを出力します。ただしShopifyにはネイティブのウィッシュリスト機能を持つアプリも多く存在するため、要件によってはアプリの利用も検討する価値があります。


Q7. 複数タブで開いているとき、一方でお気に入りを変更したらもう一方にも反映させられますか?

はい、storage イベントを使うことで実現できます。localStorageが変更されると、同じオリジンの他のタブstorage イベントが発火します。このイベントを使えば、タブ間でリアルタイムにお気に入り状態を同期できます。

// 別タブでlocalStorageが変更されたときに検知して状態を更新する
window.addEventListener('storage', function(event) {
  // お気に入りデータが変更された場合のみ処理
  if (event.key !== STORAGE_KEY) return;

  console.log('別タブでお気に入りが変更されました');
  restoreFavoriteState();   // ボタンの状態を再反映
  updateFavoriteCount();    // カウンターも更新
});

注意点として、storage イベントは変更を行ったタブ自身では発火しません。同じタブ内での変更は通常のクリックイベント処理で対応し、他のタブへの同期にだけ storage イベントを使う、という使い分けが基本です。


Q8. お気に入りデータをエクスポート・インポートできるようにしたい場合はどうすればいいですか?

localStorageのデータはJSON文字列として扱えるため、ファイルとしての書き出し・読み込みも比較的簡単に実装できます。

// エクスポート:お気に入りリストをJSONファイルとしてダウンロード
function exportFavorites() {
  const favorites = loadFavorites();
  const json = JSON.stringify(favorites, null, 2);
  const blob = new Blob([json], { type: 'application/json' });
  const url = URL.createObjectURL(blob);

  const a = document.createElement('a');
  a.href = url;
  a.download = 'favorites.json';
  a.click();
  URL.revokeObjectURL(url);
}

// インポート:JSONファイルを読み込んでお気に入りリストを復元
function importFavorites(file) {
  const reader = new FileReader();
  reader.onload = function(e) {
    try {
      const imported = JSON.parse(e.target.result);
      if (!Array.isArray(imported)) throw new Error('不正なデータ形式です');
      saveFavorites(imported);
      restoreFavoriteState();
      alert(`${imported.length}件のお気に入りをインポートしました`);
    } catch (err) {
      alert('ファイルの読み込みに失敗しました。正しいJSONファイルを選択してください。');
    }
  };
  reader.readAsText(file);
}

この機能はデバイス間のお気に入り共有や、ブラウザ移行時のデータ引き継ぎにも応用できます。

まとめ

ここまで、JavaScriptとlocalStorageを使ったお気に入り機能の実装方法を、基礎から実務レベルまで丁寧に解説してきました。最後に、実装の流れと重要なポイントを振り返っておきましょう。

お気に入り機能の本質は、「ユーザーのアクションを検知し、データを永続化し、UIに反映する」というシンプルな3ステップです。この流れさえ押さえておけば、どんなサイトにも応用が利きます。

重要ポイント

  • data-id 属性でアイテムIDをHTMLに紐付け、JavaScriptから dataset.id で取得する
  • お気に入りリストはIDの配列で管理し、JSON.stringify / JSON.parse でlocalStorageに保存・読み込みする
  • loadFavorites()saveFavorites() を関数化して、localStorageへのアクセスを一元管理する
  • ページ読み込み時に必ず restoreFavoriteState() を実行し、ボタンの状態を復元する
  • JSON.parse(localStorage.getItem('favorites')) || [] のフォールバックで、nullエラーを防ぐ
  • イベント委譲(closest())を使うことで、動的に追加された要素にも対応できる
  • localStorageのデータはブラウザ操作で消える可能性があるため、重要な用途ではサーバー側との連携も検討する

実装でつまずきやすいのは「保存はできているのに、ページ遷移後に状態が戻ってしまう」というケースです。これはほぼ確実に、状態復元処理の抜けかスクリプトの読み込みタイミングが原因です。DOMContentLoaded 内で restoreFavoriteState() を呼ぶことを、実装のチェックリストに必ず加えておいてください。

本記事のコードはすべてコピペで動作する形で提供しています。まずはサンプルコードをそのまま動かしてみて、挙動を確認してから自分のプロジェクトに合わせてカスタマイズしていくのがおすすめです。

次のステップとしては、お気に入り一覧ページの作成や、会員機能と連携したサーバーサイドへのデータ保存に挑戦してみてください。今回の実装で身につけた「配列操作・localStorageの読み書き・DOM操作」の考え方は、そのまま応用できます。

タイトルとURLをコピーしました