レビュー済み·難易度: 初級·更新: 2026-04-17

Zustand の persist middleware でローカルストレージに状態を保存する

persist middleware を使い、Zustand の状態をローカルストレージに永続化してページリロード後も状態を維持する実装例。

nextjsstate-managementzustand

対応バージョン

nextjs 15react 19zustand 5

前提環境

Zustand の基本的な store 定義と useStore の使い方を理解していること

概要

persist middleware を使うと、Zustand の state をローカルストレージに自動保存できる。 ページをリロードしても状態が復元されるため、テーマ設定・カート内容・ユーザー設定の保持に使う。

インストール

npm install zustand

実装例

// src/store/settingsStore.ts
import { create } from "zustand";
import { persist } from "zustand/middleware";

type Theme = "light" | "dark";

type SettingsStore = {
  theme: Theme;
  language: string;
  setTheme: (theme: Theme) => void;
  setLanguage: (language: string) => void;
};

export const useSettingsStore = create<SettingsStore>()(
  persist(
    (set) => ({
      theme: "light",
      language: "ja",
      setTheme: (theme) => set({ theme }),
      setLanguage: (language) => set({ language }),
    }),
    {
      name: "settings", // localStorage のキー名
      // storage オプションを指定しない場合は localStorage がデフォルト
    }
  )
);
// src/components/ThemeToggle.tsx(hydration ミスマッチを避ける実装)
"use client";

import { useEffect, useState } from "react";
import { useSettingsStore } from "@/store/settingsStore";

export function ThemeToggle() {
  const { theme, setTheme } = useSettingsStore();
  const [mounted, setMounted] = useState(false);

  // SSR と CSR の hydration ミスマッチを避けるため、mount 後に表示する
  useEffect(() => {
    setMounted(true);
  }, []);

  if (!mounted) return <div className="h-9 w-24 rounded border" />; // スケルトン

  return (
    <button
      onClick={() => setTheme(theme === "light" ? "dark" : "light")}
      className="rounded border px-3 py-2 text-sm"
    >
      現在: {theme === "light" ? "ライト" : "ダーク"}
    </button>
  );
}

sessionStorage に切り替える場合

import { create } from "zustand";
import { persist, createJSONStorage } from "zustand/middleware";

export const useSessionStore = create<{ count: number; increment: () => void }>()(
  persist(
    (set) => ({
      count: 0,
      increment: () => set((state) => ({ count: state.count + 1 })),
    }),
    {
      name: "session-count",
      storage: createJSONStorage(() => sessionStorage), // sessionStorage に切り替え
    }
  )
);

ポイント

  • persist でラップするだけで localStorage への保存・復元が自動化される
  • SSR 環境では localStorage が存在しないため、初期レンダリング時に hydration ミスマッチが発生しやすい
  • useEffect 内で mounted フラグを管理し、クライアントのみで実際の値を表示する
  • sessionStorage への切り替えは storage: createJSONStorage(() => sessionStorage) で行う
  • 保存したくないフィールドがある場合は partialize オプションで除外できる

注意点

Next.js の SSR 環境では persist による hydration ミスマッチが発生しやすい。useStore を直接使わず useEffect 内で参照するか、Zustand の skipHydration オプションで対処する。sessionStorage への切り替えは storage オプションで行う。

関連サンプル