LocalStorageでお気に入り機能を作る【Art Navi開発記録 #4】
はじめに
こんにちは。
なかのひとカンパニーの archer です。
美術展検索サービス Art Navi の開発内容を、備忘録も兼ねて記事にしています。
- サービスURL: https://art-weekend.jp/
前回は、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を本番起動する構成について書く予定です。