© TOOLS BOX — Next.js / React / TypeScript コードサンプル集

サンプルガイド
←サンプル一覧
nextjsstate-management

Zustand Test の settingsStore だけを persist で永続化する

persist middleware の partialize で settings の確定値だけを localStorage に保存し、UI 状態や派生 store を巻き込まない部分永続化を解説する。

難易度: 中級·更新: 2026-05-07·
zustand

対応バージョン

nextjs 15react 19zustand 5

前提環境

Zustand の create と middleware を理解していること

概要

Zustand Test の 4 store のうち、ユーザーの確定値が入る settingsStore だけ を localStorage に永続化したい、というのは典型的な要件です。 profileStore は同期で再生成できるので不要、detailStore は派生値なので不要、uiStore のタブ状態は永続化したくない、という割り切りです。

そこで使うのが persist middleware の partialize オプションです。store の中から「保存する範囲」を関数で絞れます。

インストール

npm install zustand

何を保存して、何を保存しないか

最初に方針を決めておきます。Playground での割り当ては次のとおり。

store永続化理由
settingsStore✅ユーザーの確定値。再ロード時に復元したい
profileStore❌settings から storeSync で再生成できる
detailStore❌suggestedRate は派生値。再計算で済む
uiStore❌タブの初期表示は固定値で良い

settingsStore の中でも action や internal な flag は保存しない。値だけを保存するのが原則です。

persist + partialize の最小実装

persist を被せ、partialize で 保存対象のキーを返す 関数を渡します。name オプションで localStorage のキーを決めます。

// src/store/settingsStore.ts
import { create } from "zustand";
import { devtools, persist, subscribeWithSelector } from "zustand/middleware";
import type { FormData } from "@/schemas/formSchema";

const INITIAL_SETTINGS: FormData = {
  name:  "",
  email: "",
  rate:  0,
};

type SettingsState = FormData & {
  patchFormData: (partial: Partial<FormData>) => void;
  setFormData:   (values: FormData) => void;
  reset:         () => void;
};

export const useSettingsStore = create<SettingsState>()(
  devtools(
    persist(
      subscribeWithSelector((set) => ({
        ...INITIAL_SETTINGS,
        patchFormData: (partial) => set(partial,           false, "patchFormData"),
        setFormData:   (values)  => set(values,            false, "setFormData"),
        reset:         ()        => set(INITIAL_SETTINGS,  true,  "reset"),
      })),
      {
        name:    "playground.settings",
        version: 1,
        partialize: (state) => ({
          name:  state.name,
          email: state.email,
          rate:  state.rate,
        }),
      }
    ),
    { name: "SettingsStore" }
  )
);

ポイントは 3 つ。

  • middleware の 重ね順は devtools(persist(subscribeWithSelector(...)))。一番外側に DevTools、次に永続化、内側に subscribe
  • partialize は 保存したいキーだけ を返す。patchFormData などの関数は除外する
  • name は localStorage のキー。プロジェクト内で衝突しないプレフィックス を付ける

「途中入力」を保存しない選択肢

Playground では patchFormData でも store が即時更新されるため、1 文字ごとに localStorage への書き込み が走ります。これを避けたい場合は、partialize で「最後に確定された値だけ」を別フィールドに持つ設計にします。

type SettingsState = FormData & {
  saved: FormData;        // ← 確定済みの値だけここに入れる
  patchFormData: (partial: Partial<FormData>) => void;
  setFormData:   (values: FormData) => void;
  reset:         () => void;
};

export const useSettingsStore = create<SettingsState>()(
  persist(
    (set) => ({
      ...INITIAL_SETTINGS,
      saved: INITIAL_SETTINGS,
      patchFormData: (partial) => set(partial),
      setFormData:   (values)  => set({ ...values, saved: values }),
      reset:         ()        => set({ ...INITIAL_SETTINGS, saved: INITIAL_SETTINGS }),
    }),
    {
      name:       "playground.settings",
      version:    1,
      partialize: (s) => ({ saved: s.saved }), // ← saved だけ保存
    }
  )
);

復元時に saved から現在値を組み立てる手間が増えますが、localStorage の書き込み頻度が「保存ボタン押下時のみ」 に下がります。

migration: スキーマが変わったら version を上げる

スキーマに rate を追加するなど 保存形式が変わった ときは、version を上げて migrate 関数を書きます。古い形式の値が残っていても安全に読み戻せます。

persist(
  (set) => ({ /* ... */ }),
  {
    name:    "playground.settings",
    version: 2,                                    // ← 旧 1 → 2 に上げた
    partialize: (s) => ({
      name:  s.name,
      email: s.email,
      rate:  s.rate,
    }),
    migrate: (persisted, fromVersion) => {
      const obj = persisted as Record<string, unknown>;
      if (fromVersion < 2) {
        return {
          name:  String(obj.name  ?? ""),
          email: String(obj.email ?? ""),
          rate:  0,                                // 旧値に rate がなければ 0
        };
      }
      return obj as never;
    },
  }
)

migrate は persist が読み戻した直後・hydration の前 に呼ばれます。返したオブジェクトが partialize の形と一致している必要があります。

hydration mismatch を避ける

Next.js の Server Component と組み合わせると、サーバーは初期値、クライアントは localStorage の復元値、という 乖離 で hydration mismatch が発生しがちです。回避策の典型は次の 2 つ。

  1. クライアントでだけ store を参照する("use client" の境界を明示)
  2. 復元完了まで描画を抑える(onRehydrateStorage か useState のフラグで制御)
"use client";
import { useEffect, useState } from "react";
import { useSettingsStore } from "@/store/settingsStore";

export function HydratedNameDisplay() {
  const [hydrated, setHydrated] = useState(false);
  const name = useSettingsStore((s) => s.name);

  useEffect(() => setHydrated(true), []);
  if (!hydrated) return null; // SSR と揃えるため最初は描画しない

  return <p>{name}</p>;
}

詳しくは RSC 境界編 を参照してください。

「全ストアをリセット」と persist の関係

reset は state を初期値に戻しますが、localStorage の値は自動で消えません。reset 後に同じ state が再保存されるため、結果的に「初期値が保存される」状態になります。

確実にクリアしたい場合は persist の API を使います。

useSettingsStore.persist.clearStorage();

「全ストアをリセット」ボタンの中で reset() の後に clearStorage() を呼ぶと、メモリと localStorage の両方を初期化できます。

ポイント

  • 永続化したいのは 値だけ。partialize で対象キーを明示し、関数は除外する
  • middleware は devtools(persist(subscribeWithSelector(...))) の順に重ねる
  • 1 文字ごとの書き込みを避けたいなら、確定値だけ持つフィールド を切り出して partialize で絞る
  • スキーマ変更時は version を上げて migrate を書く。旧データが残っていても安全に読み戻せる
  • SSR と組み合わせるときは hydration mismatch に注意。クライアント境界 で参照するか復元完了まで描画を抑える

関連記事

セットで読むと文脈がつながる関連記事は次のとおりです。

  • Zustand 編 — 4 store の役割分担
  • RSC 境界編 — "use client" と hydration の境界
  • devtools 命名規則編 — middleware を重ねるときの順序
  • Zustand Test (Build) — 完成例と外部デモ

注意点

Playground 追加記事。永続化したい範囲を partialize で絞り、versionアップ時の migrate を最初から仕込む書き方を整理する。

リポジトリを見る

関連サンプル

同じテーマや技術スタックを使った実装例

  • Zustand Test のアクションを selector → props で安全に受け渡す

    Zustand のアクションは store 生成時から参照が安定している性質を活かし、selector で取り出して props で子に渡す書き方を解説する。

  • Zustand Test の保存処理を非同期 action と loading / error 状態で扱う

    saveSettings のような非同期処理を store のアクションに閉じ込め、status: 'idle'|'loading'|'error'|'success' を 1 つのフィールドで表現する書き方を解説する。

  • Zustand Test の settingsStore を BroadcastChannel で複数タブに同期する

    BroadcastChannel API で別タブの settingsStore に変更をブロードキャストし、エコーバックを避けながら受信側を更新する手順を解説する。

  • Zustand Test の suggestedRate を subscribeWithSelector で派生する

    settingsStore.rate から detailStore.suggestedRate を導出する派生状態の作り方を、subscribeWithSelector と shallow を軸に解説する。

  • Zustand Test の devtools をアクション命名規則で読みやすくする

    store 名 / アクション名 / replace モードを揃え、Redux DevTools のタイムラインを 4 store 横断で読み解けるようにする命名規則を解説する。

関連仕様

このサンプルを理解するのに役立つ仕様や概念

  • FrameworkNext.jsReact ベースのフルスタックフレームワーク。SSR・SSG・App Router・API Routes を提供する。
←サンプル一覧に戻る