0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

LocalStorageでお気に入り機能を作る【Art Navi開発記録 #4】

0
Last updated at Posted at 2026-07-02

LocalStorageでお気に入り機能を作る【Art Navi開発記録 #4】

はじめに

こんにちは。
なかのひとカンパニーの archer です。

美術展検索サービス Art Navi の開発内容を、備忘録も兼ねて記事にしています。

前回は、Next.js + PostgreSQL + Docker の開発環境について書きました。

今回は、Art Navi の お気に入り機能 についてまとめます。

MVP段階ではユーザー認証を入れず、ブラウザの LocalStorage を使ってお気に入りを保存する方針にしました。

なぜLocalStorageにしたか

お気に入り機能を作る場合、一般的にはユーザー登録やログイン機能を用意して、DBにお気に入り情報を保存する構成が考えられます。

ただ、MVP段階でそこまで作ると、実装範囲がかなり広がります。

認証を入れると、以下のような対応も必要になります。

  • 会員登録
  • ログイン
  • ログアウト
  • セッション管理
  • パスワード再設定
  • 退会処理

Art Navi では、まずは 気になった展示を一時的に保存できること を優先しました。

そのため、最初はログイン不要で使える LocalStorage を使うことにしました。

今回作るもの

今回作る機能は以下です。

  • 展覧会をお気に入りに追加する
  • お気に入り済みかどうかを判定する
  • お気に入りから削除する
  • お気に入りページで一覧表示する

お気に入り一覧ページは、以下のURLで用意します。

/favorites

保存するデータ

MVPでは、LocalStorageに保存する情報をできるだけシンプルにします。

今回は、展覧会IDだけを保存する形にしました。

[1, 2, 5]

展覧会名や会場名も保存する方法はありますが、情報が更新されたときにLocalStorage内のデータが古くなる可能性があります。

そのため、LocalStorageにはIDだけを保存し、表示時に展覧会データと照合する方針にしました。

LocalStorageのキー

LocalStorageのキーは、サービス名を含めて分かりやすくします。

const FAVORITE_KEY = "art-navi:favorites";

サービス名を入れておくと、他の機能や別サービスのデータと衝突しにくくなります。

お気に入り操作用の関数

お気に入りの取得・追加・削除は、共通関数にまとめます。

const FAVORITE_KEY = "art-navi:favorites";

export function getFavoriteIds(): number[] {
  if (typeof window === "undefined") {
    return [];
  }

  const value = window.localStorage.getItem(FAVORITE_KEY);

  if (!value) {
    return [];
  }

  try {
    return JSON.parse(value);
  } catch {
    return [];
  }
}

export function saveFavoriteIds(ids: number[]) {
  window.localStorage.setItem(FAVORITE_KEY, JSON.stringify(ids));
}

export function isFavorite(id: number): boolean {
  return getFavoriteIds().includes(id);
}

export function addFavorite(id: number) {
  const ids = getFavoriteIds();

  if (ids.includes(id)) {
    return;
  }

  saveFavoriteIds([...ids, id]);
}

export function removeFavorite(id: number) {
  const ids = getFavoriteIds();

  saveFavoriteIds(ids.filter((favoriteId) => favoriteId !== id));
}

typeof window === "undefined" を入れているのは、Next.jsではサーバー側で実行される可能性があるためです。

LocalStorageはブラウザの機能なので、サーバー側では使えません。

お気に入りボタンを作る

次に、お気に入りボタンを作ります。

LocalStorageを使う処理はブラウザ側で動かす必要があるため、Client Component として作ります。

"use client";

import { useEffect, useState } from "react";
import {
  addFavorite,
  isFavorite,
  removeFavorite,
} from "@/lib/favorites";

type FavoriteButtonProps = {
  exhibitionId: number;
};

export function FavoriteButton({ exhibitionId }: FavoriteButtonProps) {
  const [favorite, setFavorite] = useState(false);

  useEffect(() => {
    setFavorite(isFavorite(exhibitionId));
  }, [exhibitionId]);

  const handleClick = () => {
    if (favorite) {
      removeFavorite(exhibitionId);
      setFavorite(false);
    } else {
      addFavorite(exhibitionId);
      setFavorite(true);
    }
  };

  return (
    <button type="button" onClick={handleClick}>
      {favorite ? "お気に入り済み" : "お気に入りに追加"}
    </button>
  );
}

"use client" を付けることで、このコンポーネントはブラウザ側で動きます。

初回表示時に useEffect でLocalStorageを読み込み、お気に入り済みかどうかを反映しています。

一覧ページで使う

展覧会一覧ページでは、各カードの中に FavoriteButton を配置します。

import { FavoriteButton } from "@/components/FavoriteButton";

export default function ExhibitionsPage() {
  return (
    <main>
      <h1>展覧会一覧</h1>

      {exhibitions.map((exhibition) => (
        <article key={exhibition.id}>
          <h2>{exhibition.title}</h2>
          <p>{exhibition.venue}</p>

          <FavoriteButton exhibitionId={exhibition.id} />
        </article>
      ))}
    </main>
  );
}

これで、一覧画面から気になった展覧会をお気に入りに追加できます。

お気に入りページを作る

次に、お気に入り一覧ページを作ります。

app/
  favorites/
    page.tsx

LocalStorageを使うため、このページもClient Componentとして作ります。

"use client";

import { useEffect, useState } from "react";
import Link from "next/link";
import { getFavoriteIds } from "@/lib/favorites";

const exhibitions = [
  {
    id: 1,
    title: "印象派と光の表現",
    venue: "上野の美術館",
  },
  {
    id: 2,
    title: "現代アートの現在地",
    venue: "六本木の美術館",
  },
];

export default function FavoritesPage() {
  const [favoriteIds, setFavoriteIds] = useState<number[]>([]);

  useEffect(() => {
    setFavoriteIds(getFavoriteIds());
  }, []);

  const favoriteExhibitions = exhibitions.filter((exhibition) =>
    favoriteIds.includes(exhibition.id)
  );

  return (
    <main>
      <h1>お気に入り</h1>

      {favoriteExhibitions.length === 0 ? (
        <p>お気に入りに追加した展覧会はまだありません。</p>
      ) : (
        favoriteExhibitions.map((exhibition) => (
          <article key={exhibition.id}>
            <h2>{exhibition.title}</h2>
            <p>{exhibition.venue}</p>

            <Link href={`/exhibitions/${exhibition.id}`}>
              詳細を見る
            </Link>
          </article>
        ))
      )}
    </main>
  );
}

これで、LocalStorageに保存したIDをもとに、お気に入り登録済みの展覧会だけを表示できます。

LocalStorageで気をつけること

LocalStorageは手軽ですが、注意点もあります。

  • ブラウザごとに保存される
  • 別端末には同期されない
  • ユーザーがブラウザデータを削除すると消える
  • サーバー側では参照できない
  • 大量データの保存には向かない

そのため、ユーザーごとの永続的な保存にはDBを使う方が向いています。

ただ、MVP段階で「一時的に気になる展示を保存する」用途であれば、LocalStorageでも十分だと判断しました。

実装してみてよかった点

LocalStorageで実装したことで、以下のメリットがありました。

  • ログインなしで使える
  • 実装がシンプル
  • DB設計を後回しにできる
  • MVPの確認が早い
  • ユーザー体験が軽い

特に、会員登録なしで使える点は大きいです。

気になった展示を保存するだけなら、最初からログインを求めない方が自然だと感じました。

今後やりたいこと

今後、お気に入り機能では以下を改善していきたいです。

  • お気に入り解除ボタンのUI改善
  • 一覧ページと詳細ページの状態同期
  • お気に入り数の表示
  • 開催終了した展示の表示制御
  • ログイン機能追加時のDB保存
  • 複数端末でのお気に入り同期

まずはMVPとしてLocalStorageで実装し、必要になったタイミングでDB保存に切り替える予定です。

まとめ

今回は、Art Navi のお気に入り機能についてまとめました。

MVP段階では、ユーザー認証を入れずに LocalStorage を使って実装しています。

今回の方針は以下です。

  • LocalStorageには展覧会IDだけ保存する
  • お気に入りボタンはClient Componentで作る
  • 一覧ページや詳細ページからお気に入り登録できるようにする
  • /favorites でお気に入り一覧を表示する

最初から認証やDB保存まで作り込まず、まずはサービスとして触れる状態を優先しました。

次回は、Docker ComposeでNext.jsを本番起動する構成について書く予定です。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?