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

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

Next.js 15 + Zustand でモーダル状態管理を作る

Zustand を使ってモーダルの開閉状態をグローバルに管理する実装例。複数モーダルへの対応と型安全な設計を示す。

難易度: 中級·更新: 2026-04-14·
zustandtailwindcss

対応バージョン

nextjs 15react 19typescript 5zustand 5tailwindcss 4

前提環境

React の基本的な hooks の使い方を理解していること

概要

Zustand を使ってモーダルの開閉状態をグローバルに管理する。 複数モーダルを識別するため modalKey を使った設計にする。

インストール

npm install zustand

ストア定義

// src/stores/modalStore.ts
import { create } from "zustand";

type ModalKey = "confirm" | "profile" | "settings";

type ModalState = {
  openModals: Set<ModalKey>;
  open: (key: ModalKey) => void;
  close: (key: ModalKey) => void;
  isOpen: (key: ModalKey) => boolean;
};

export const useModalStore = create<ModalState>((set, get) => ({
  openModals: new Set(),
  open: (key) =>
    set((state) => ({ openModals: new Set(state.openModals).add(key) })),
  close: (key) => {
    const next = new Set(get().openModals);
    next.delete(key);
    set({ openModals: next });
  },
  isOpen: (key) => get().openModals.has(key),
}));

モーダルコンポーネント

// src/components/Modal.tsx
"use client";

import { useModalStore } from "@/stores/modalStore";

type Props = {
  modalKey: "confirm" | "profile" | "settings";
  title: string;
  children: React.ReactNode;
};

export function Modal({ modalKey, title, children }: Props) {
  const { isOpen, close } = useModalStore();

  if (!isOpen(modalKey)) return null;

  return (
    <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
      <div className="w-full max-w-md rounded-lg bg-white p-6 shadow-xl">
        <div className="mb-4 flex items-center justify-between">
          <h2 className="text-lg font-semibold">{title}</h2>
          <button
            onClick={() => close(modalKey)}
            className="text-gray-400 hover:text-gray-600"
            aria-label="閉じる"
          >
            ✕
          </button>
        </div>
        {children}
      </div>
    </div>
  );
}

利用例

"use client";

import { useModalStore } from "@/stores/modalStore";
import { Modal } from "@/components/Modal";

export function Page() {
  const { open } = useModalStore();

  return (
    <>
      <button onClick={() => open("confirm")}>確認モーダルを開く</button>
      <Modal modalKey="confirm" title="確認">
        <p>本当に実行しますか?</p>
      </Modal>
    </>
  );
}

ポイント

  • Set で複数モーダルを管理し、互いに干渉しない
  • isOpen を selector として使うことで必要なコンポーネントだけ再レンダリングされる

関連サンプル

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

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

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

  • Zustand Test の Store Monitor サイドパネルを selector だけで作る

    全 store の値をリアルタイムに監視するサイドパネルを selector だけで構成し、再レンダリング範囲を局所化するパターンを解説する。

  • Zustand Test の Form / Monitor / TabBar を Storybook で分離する

    Zustand に依存する UI を Storybook で取り扱うために、store のリセット / decorator / play function を組み合わせて状態をストーリー単位で固定する手順を解説する。

  • Zustand Test の uiStore でタブ切り替えだけを切り出す

    アクティブなタブの状態だけを抱える uiStore を独立させ、settings / profile / detail と疎結合に保つ最小設計を解説する。

  • Next.js Intercepting Routes で URL 付きモーダルを実装する

    Intercepting Routes((..) 記法)と Parallel Routes を組み合わせ、リスト画面の URL を維持したままモーダルで詳細を表示する実装例。

関連仕様

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

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