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

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

Zustand Test の settingsStore を slice と combine で分割する

肥大化しがちな store を「フィールドのスライス」「アクションのスライス」に分けて combine で結合し、責務をモジュール単位で表現する手順を解説する。

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

対応バージョン

nextjs 15react 19zustand 5

前提環境

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

概要

Zustand Test の settingsStore は state 3 件・action 3 件の小規模ですが、現実のフォームでは state 10 件・action 10 件 ぐらいに膨らみます。1 ファイルにまとめると create のコールバックが見通しづらくなるため、slice 単位で分割 し combine で結合する書き方を採ります。

この記事で扱うのは次の 3 点。

  • slice の最小単位: 「責務のあるフィールド + そのフィールドに対する action」を 1 ファイルに閉じる
  • combine でひとつの store にまとめる: state 形を渡すだけで型推論が効く
  • slice 同士の依存: 直接 import するか、外部から関数で渡すか

インストール

npm install zustand

slice の最小単位

slice は「state の一部 + その action」を返す関数です。StateCreator を使うと middleware 連鎖時にも型が壊れません(StateCreator 編 も参照)。

// src/store/slices/identitySlice.ts
import type { StateCreator } from "zustand";

export type IdentitySlice = {
  name:    string;
  email:   string;
  setName:  (name: string) => void;
  setEmail: (email: string) => void;
};

export const createIdentitySlice: StateCreator<
  IdentitySlice,
  [],
  [],
  IdentitySlice
> = (set) => ({
  name:     "",
  email:    "",
  setName:  (name)  => set({ name }),
  setEmail: (email) => set({ email }),
});
// src/store/slices/rateSlice.ts
import type { StateCreator } from "zustand";

export type RateSlice = {
  rate:    number;
  setRate: (rate: number) => void;
};

export const createRateSlice: StateCreator<
  RateSlice,
  [],
  [],
  RateSlice
> = (set) => ({
  rate:    0,
  setRate: (rate) => set({ rate: Math.min(100, Math.max(0, rate)) }),
});

各 slice は 自分の type と creator を 1 つずつ export します。テストも slice 単位で書けます。

combine で結合する

combine は state の初期値を受け取り、set / get / store で残りの slice を組み立てる ための middleware です。

// src/store/settingsStore.ts
import { create } from "zustand";
import { combine, devtools, subscribeWithSelector } from "zustand/middleware";
import { createIdentitySlice } from "./slices/identitySlice";
import { createRateSlice }     from "./slices/rateSlice";

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

export const useSettingsStore = create<
  typeof INITIAL & ReturnType<typeof commonActions>
>()(
  devtools(
    subscribeWithSelector(
      combine(INITIAL, (set, get, store) => ({
        ...createIdentitySlice(set, get, store),
        ...createRateSlice(set, get, store),
        ...commonActions(set),
      }))
    ),
    { name: "SettingsStore" }
  )
);

function commonActions(set: (v: Partial<typeof INITIAL>) => void) {
  return {
    reset: () => set(INITIAL),
  };
}

combine を使うと 「初期値部分の型」と「アクション部分の型」を別々に書ける ので、SettingsState を手動で定義しなくて済みます。型は typeof INITIAL & ReturnType<...> の交差で表現されます。

slice 同士の依存をどう扱うか

slice A から slice B の値を読みたいときは、get() 経由で同じ store を参照 します。ただし、依存関係が双方向になると追いづらくなるため、依存は 片方向 に限定するのが原則です。

// 例: identitySlice が rateSlice の rate を見たい場合
export const createIdentitySlice: StateCreator<
  IdentitySlice & RateSlice,         // ← 依存先の型を交差で渡す
  [],
  [],
  IdentitySlice
> = (set, get) => ({
  name:     "",
  email:    "",
  setName:  (name)  => {
    const rate = get().rate;          // ← 別 slice の値を読める
    set({ name: rate > 50 ? name.toUpperCase() : name });
  },
  setEmail: (email) => set({ email }),
});

ここで重要なのは 依存先の型を StateCreator の第 1 ジェネリクスに含める こと。これがないと get().rate で型エラーになります。

どこまで slice にするか

「全部 slice にすべき」ではありません。判断基準は次のとおり。

状況推奨
1 ファイルが 100 行を超え始めたslice に分け始める
同じグループの state / action が増えるグループ単位で slice 化
1 つの slice しか触らない機能を追加したその slice ファイルだけ修正で済む
store 全体で 30 行未満slice にせず 1 ファイルで OK

Playground の 4 store のうち、settingsStore だけがこの slice 構成のメリットを享受できる規模になりがちです。uiStore / detailStore は slice にしない方がシンプル に保てます。

ポイント

  • slice は 「state の一部 + その action」 を 1 ファイルに閉じる単位。StateCreator で型を書く
  • combine(INITIAL, (set, get) => ({ ... })) で初期値とアクションを別々に与え、型の交差で全体を表現する
  • slice 間の依存は 片方向。get() 経由で別 slice の値を読むときは StateCreator の型に依存先を含める
  • すべてを slice にしない。1 ファイル 100 行未満 なら slice にせず読みやすさを優先する
  • middleware を被せる場合は devtools(subscribeWithSelector(combine(...))) の順に重ねる

関連記事

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

  • Zustand 編 — 単一ファイルの基本構成
  • StateCreator 編 — middleware 連鎖時の型ヘルパ
  • immer 編 — slice の中で深いネストを更新するなら
  • Zustand Test (Build) — 完成例と外部デモ

注意点

Playground 追加記事。1 つの store が肥大化したとき、ファイル分割と型再利用を slice + combine でどう組むかをまとめる。

リポジトリを見る

関連サンプル

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

  • Zustand Test の middleware 連鎖を StateCreator で型崩れなく書く

    devtools / persist / subscribeWithSelector / immer を重ねたときに set の引数推論が壊れないよう、StateCreator のジェネリクスで型を補強する手順を解説する。

  • 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 を軸に解説する。

関連仕様

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

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