7
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Claude Code x MVP開発に最適なNext.jsの状態管理パターン [Zustand, React Hook Form, TanStack Query]

Last updated at Posted at 2025-07-15

本記事では、シンプルで保守性の高い状態管理アーキテクチャを実現するための実践的なガイドを紹介します。
docsファイルとしてそのままLLMに与えることができます。

用途としては、「Vibe Coding による MVP 開発」を想定しているため、下記を重視します。

  • シンプルさ
  • AI フレンドリー
  • 複雑な要件は実装しない
  • 高度なパフォーマンス最適化は必要としない
    zustand + react-hook-form で十分かつ
    最適ではないかと。

docs/state-management.md

# State Management Guide

## Core Principles

- Zustand: Global state management
- React Hook Form: Form state management
- useState: Component state
- No other libraries (Context, Redux, Jotai, etc.)

## State Classification

| Type            | Tool            | Examples                         |
| --------------- | --------------- | -------------------------------- |
| Global State    | Zustand         | Wallet connection, notifications |
| Form State      | React Hook Form | Input, validation                |
| Component State | useState        | Modals, local UI                 |
| Server State    | TanStack Query  | API data (future)                |

## Zustand Store Design

### Domain-Based Separation

```typescript
src/stores/
├── mintStore.ts      // Mint functionality
├── walletStore.ts    // Wallet connection
├── uiStore.ts        // UI state
└── nftStore.ts       // NFT data
```

### Store Structure

```typescript
import { create } from "zustand";
import { devtools } from "zustand/middleware";

interface MintStore {
  mintingState: "idle" | "minting" | "success" | "error";
  mintedNftId: string | null;
  errorMessage: string | null;
  mintAgent: () => Promise<void>;
  resetMintState: () => void;
}

export const useMintStore = create<MintStore>()(
  devtools(
    (set, get) => ({
      mintingState: "idle",
      mintedNftId: null,
      errorMessage: null,

      mintAgent: async () => {
        try {
          set({ mintingState: "minting", errorMessage: null });
          // mint logic
          set({ mintingState: "success" });
        } catch (error) {
          set({
            mintingState: "error",
            errorMessage: error.message,
          });
        }
      },

      resetMintState: () => {
        set({
          mintingState: "idle",
          mintedNftId: null,
          errorMessage: null,
        });
      },
    }),
    { name: "mint-store" }
  )
);
```

### Persistence (When Needed)

```typescript
import { persist } from "zustand/middleware";

export const useUIStore = create<UIStore>()(
  devtools(
    persist(
      (set, get) => ({
        sidebarOpen: false,
        theme: "light",
        // ...
      }),
      {
        name: "ui-store",
        partialize: (state) => ({
          sidebarOpen: state.sidebarOpen,
          theme: state.theme,
        }),
      }
    ),
    { name: "ui-store" }
  )
);
```

### Retry Functionality

```typescript
retryMint: async () => {
  const { retryCount, maxRetries } = get();

  if (retryCount >= maxRetries) {
    set({ errorMessage: "Max retries exceeded", mintingState: "error" });
    return;
  }

  set({ retryCount: retryCount + 1 });
  const delay = Math.min(1000 * Math.pow(2, retryCount), 10000);
  await new Promise(resolve => setTimeout(resolve, delay));

  await get().mintAgent();
},
```

## React Hook Form Integration

### Custom Hook

```typescript
export const useMintForm = () => {
  const {
    selectedStrategy,
    mintPrice,
    updateConfig,
    estimateGas,
    isWalletConnected,
  } = useMintStore();

  useEffect(() => {
    if (isWalletConnected && selectedStrategy && mintPrice) {
      estimateGas();
    }
  }, [isWalletConnected, selectedStrategy, mintPrice, estimateGas]);

  const handleConfigChange = (field: string, value: string) => {
    updateConfig({ [field]: value });
  };

  const isFormValid = selectedStrategy && mintPrice && isWalletConnected;

  return {
    selectedStrategy,
    mintPrice,
    isFormValid,
    handleConfigChange,
  };
};
```

### Form Usage Example

```tsx
export const AgentConfiguration = () => {
  const { selectedStrategy, mintPrice, isFormValid, handleConfigChange } =
    useMintForm();

  return (
    <form>
      <Select
        value={selectedStrategy}
        onValueChange={(value) => handleConfigChange("strategy", value)}
      >
        {/* options */}
      </Select>
      <Input
        value={mintPrice}
        onChange={(e) => handleConfigChange("mintPrice", e.target.value)}
      />
      <Button disabled={!isFormValid}>Mint Agent</Button>
    </form>
  );
};
```

## Next.js App Router Support

### Server/Client Components Separation

```tsx
// Server Component
export default function MintPage() {
  return (
    <div>
      <h1>Mint Agent</h1>
      <MintUIPanel />
    </div>
  );
}

// Client Component
("use client");
export const MintUIPanel = () => {
  const { mintingState, mintAgent } = useMintStore();
  // ...
};
```

### SSR Support

```typescript
"use client";
export const WalletStatus = () => {
  const [mounted, setMounted] = useState(false);
  const { isWalletConnected, walletAddress } = useWalletStore();

  useEffect(() => {
    setMounted(true);
  }, []);

  if (!mounted) {
    return <div>Loading...</div>;
  }

  return (
    <div>
      {isWalletConnected ? (
        <span>Connected: {walletAddress}</span>
      ) : (
        <span>Not connected</span>
      )}
    </div>
  );
};
```

## Notification System

```typescript
addNotification: (notification) => {
  const id = `notification-${Date.now()}`;
  const newNotification = { ...notification, id, timestamp: Date.now() };

  set((state) => ({
    notifications: [...state.notifications, newNotification],
  }));

  if (notification.duration !== 0) {
    const duration = notification.duration || 5000;
    setTimeout(() => {
      get().removeNotification(id);
    }, duration);
  }
},
```

## Performance Optimization

### Selector Pattern

```typescript
// ✅ Good: Subscribe to specific state only
const useMintingStatus = () =>
  useMintStore((state) => ({
    mintingState: state.mintingState,
    errorMessage: state.errorMessage,
  }));

// ❌ Bad: Subscribe to entire store
const store = useMintStore();
```

### React.memo Optimization

```tsx
import { memo } from "react";

export const WalletStatus = memo(() => {
  const isConnected = useWalletStore((state) => state.isConnected);
  return <div>{isConnected ? "Connected" : "Disconnected"}</div>;
});
```

## Error Handling

```typescript
const handleAsyncAction = async (
  actionName: string,
  action: () => Promise<void>
) => {
  try {
    set({ [`${actionName}Loading`]: true, [`${actionName}Error`]: null });
    await action();
    set({ [`${actionName}Loading`]: false });
  } catch (error) {
    const errorMessage =
      error instanceof Error ? error.message : `${actionName} failed`;
    set({
      [`${actionName}Loading`]: false,
      [`${actionName}Error`]: errorMessage,
    });

    useUIStore.getState().addNotification({
      type: "error",
      title: `${actionName} Failed`,
      message: errorMessage,
    });
  }
};
```

## Summary

### Recommended

- Separate stores by feature
- Use DevTools middleware
- Minimal persistence
- Custom Hooks for Zustand + React Hook Form integration
- Selectors for specific state subscription
- Unified error handling

### Prohibited

- Multiple library usage (Context, Redux, Jotai, etc.)
- Store bloat (all features in one store)
- Direct set() calls from outside
- Full state localStorage persistence

docs/ja/state-management.md

# State Management ガイド

## 基本原則

- Zustand: グローバル状態管理
- React Hook Form: フォーム状態管理
- useState: コンポーネント状態
- 他ライブラリ併用禁止(Context, Redux, Jotai 等)

## 状態分類

| 種類               | ツール          | 例                    |
| ------------------ | --------------- | --------------------- |
| グローバル状態     | Zustand         | Wallet 接続、通知     |
| フォーム状態       | React Hook Form | 入力、バリデーション  |
| コンポーネント状態 | useState        | モーダル、ローカル UI |
| サーバー状態       | TanStack Query  | API データ(将来)    |

## Zustand Store 設計

### ドメイン別分割

```typescript
src/stores/
├── mintStore.ts      // Mint機能
├── walletStore.ts    // Wallet接続
├── uiStore.ts        // UI状態
└── nftStore.ts       // NFTデータ
```

### Store 構造

```typescript
import { create } from "zustand";
import { devtools } from "zustand/middleware";

interface MintStore {
  mintingState: "idle" | "minting" | "success" | "error";
  mintedNftId: string | null;
  errorMessage: string | null;
  mintAgent: () => Promise<void>;
  resetMintState: () => void;
}

export const useMintStore = create<MintStore>()(
  devtools(
    (set, get) => ({
      mintingState: "idle",
      mintedNftId: null,
      errorMessage: null,

      mintAgent: async () => {
        try {
          set({ mintingState: "minting", errorMessage: null });
          // mint logic
          set({ mintingState: "success" });
        } catch (error) {
          set({
            mintingState: "error",
            errorMessage: error.message,
          });
        }
      },

      resetMintState: () => {
        set({
          mintingState: "idle",
          mintedNftId: null,
          errorMessage: null,
        });
      },
    }),
    { name: "mint-store" }
  )
);
```

### 永続化(必要時のみ)

```typescript
import { persist } from "zustand/middleware";

export const useUIStore = create<UIStore>()(
  devtools(
    persist(
      (set, get) => ({
        sidebarOpen: false,
        theme: "light",
        // ...
      }),
      {
        name: "ui-store",
        partialize: (state) => ({
          sidebarOpen: state.sidebarOpen,
          theme: state.theme,
        }),
      }
    ),
    { name: "ui-store" }
  )
);
```

### リトライ機能

```typescript
retryMint: async () => {
  const { retryCount, maxRetries } = get();

  if (retryCount >= maxRetries) {
    set({ errorMessage: "Max retries exceeded", mintingState: "error" });
    return;
  }

  set({ retryCount: retryCount + 1 });
  const delay = Math.min(1000 * Math.pow(2, retryCount), 10000);
  await new Promise(resolve => setTimeout(resolve, delay));

  await get().mintAgent();
},
```

## React Hook Form 連携

### Custom Hook

```typescript
export const useMintForm = () => {
  const {
    selectedStrategy,
    mintPrice,
    updateConfig,
    estimateGas,
    isWalletConnected,
  } = useMintStore();

  useEffect(() => {
    if (isWalletConnected && selectedStrategy && mintPrice) {
      estimateGas();
    }
  }, [isWalletConnected, selectedStrategy, mintPrice, estimateGas]);

  const handleConfigChange = (field: string, value: string) => {
    updateConfig({ [field]: value });
  };

  const isFormValid = selectedStrategy && mintPrice && isWalletConnected;

  return {
    selectedStrategy,
    mintPrice,
    isFormValid,
    handleConfigChange,
  };
};
```

### フォーム使用例

```tsx
export const AgentConfiguration = () => {
  const { selectedStrategy, mintPrice, isFormValid, handleConfigChange } =
    useMintForm();

  return (
    <form>
      <Select
        value={selectedStrategy}
        onValueChange={(value) => handleConfigChange("strategy", value)}
      >
        {/* options */}
      </Select>
      <Input
        value={mintPrice}
        onChange={(e) => handleConfigChange("mintPrice", e.target.value)}
      />
      <Button disabled={!isFormValid}>Mint Agent</Button>
    </form>
  );
};
```

## Next.js App Router 対応

### Server/Client Components 使い分け

```tsx
// Server Component
export default function MintPage() {
  return (
    <div>
      <h1>Mint Agent</h1>
      <MintUIPanel />
    </div>
  );
}

// Client Component
("use client");
export const MintUIPanel = () => {
  const { mintingState, mintAgent } = useMintStore();
  // ...
};
```

### SSR 対応

```typescript
"use client";
export const WalletStatus = () => {
  const [mounted, setMounted] = useState(false);
  const { isWalletConnected, walletAddress } = useWalletStore();

  useEffect(() => {
    setMounted(true);
  }, []);

  if (!mounted) {
    return <div>Loading...</div>;
  }

  return (
    <div>
      {isWalletConnected ? (
        <span>Connected: {walletAddress}</span>
      ) : (
        <span>Not connected</span>
      )}
    </div>
  );
};
```

## 通知システム

```typescript
addNotification: (notification) => {
  const id = `notification-${Date.now()}`;
  const newNotification = { ...notification, id, timestamp: Date.now() };

  set((state) => ({
    notifications: [...state.notifications, newNotification],
  }));

  if (notification.duration !== 0) {
    const duration = notification.duration || 5000;
    setTimeout(() => {
      get().removeNotification(id);
    }, duration);
  }
},
```

## Performance最適化

### Selectorパターン

```typescript
// ✅ Good: 必要な状態のみ購読
const useMintingStatus = () =>
  useMintStore((state) => ({
    mintingState: state.mintingState,
    errorMessage: state.errorMessage,
  }));

// ❌ Bad: Store全体購読
const store = useMintStore();
```

### React.memo 最適化

```tsx
import { memo } from "react";

export const WalletStatus = memo(() => {
  const isConnected = useWalletStore((state) => state.isConnected);
  return <div>{isConnected ? "Connected" : "Disconnected"}</div>;
});
```

## エラーハンドリング

```typescript
const handleAsyncAction = async (
  actionName: string,
  action: () => Promise<void>
) => {
  try {
    set({ [`${actionName}Loading`]: true, [`${actionName}Error`]: null });
    await action();
    set({ [`${actionName}Loading`]: false });
  } catch (error) {
    const errorMessage =
      error instanceof Error ? error.message : `${actionName} failed`;
    set({
      [`${actionName}Loading`]: false,
      [`${actionName}Error`]: errorMessage,
    });

    useUIStore.getState().addNotification({
      type: "error",
      title: `${actionName} Failed`,
      message: errorMessage,
    });
  }
};
```

## まとめ

### 推奨

- Store を機能別に分割
- DevTools middleware 使用
- 必要最小限の永続化
- Custom Hooks で Zustand と React Hook Form 連携
- Selector で必要な状態のみ購読
- 統一されたエラーハンドリング

### 禁止

- 複数ライブラリ併用 (Context, Redux, Jotai 等)
- Store 肥大化 (1 つの Store に全機能)
- set()の外部から直接呼び出し
- 全状態の localStorage 保存

参考: シリーズ記事

Claude Codeを使用した開発フロー(特にdApps)に最適なテンプレートを構築中で、各項目についてメモを残しています。

  1. Claude Code x MVP開発に最適なdApps要求定義
  2. Claude Code x MVP開発に最適なNext.jsディレクトリ構成
  3. Claude Code x MVP開発に最適なUXデザインガイド
  4. Claude Code x MVP開発に最適なNext.jsの状態管理パターン [Zustand, React Hook Form, TanStack Query]
  5. Claude Code x MVP開発の作業フロー(のメモ)
  6. AO dApps × Next.jsのnpmライブラリ選定 [2025年]
  7. EVM dApps x Next.jsのnpmライブラリ選定 [2025 年]
  8. Solana dApps × Next.jsのnpmライブラリ選定 [2025年]
7
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?