本記事では、シンプルで保守性の高い状態管理アーキテクチャを実現するための実践的なガイドを紹介します。
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)に最適なテンプレートを構築中で、各項目についてメモを残しています。
- Claude Code x MVP開発に最適なdApps要求定義
- Claude Code x MVP開発に最適なNext.jsディレクトリ構成
- Claude Code x MVP開発に最適なUXデザインガイド
- Claude Code x MVP開発に最適なNext.jsの状態管理パターン [Zustand, React Hook Form, TanStack Query]
- Claude Code x MVP開発の作業フロー(のメモ)
- AO dApps × Next.jsのnpmライブラリ選定 [2025年]
- EVM dApps x Next.jsのnpmライブラリ選定 [2025 年]
- Solana dApps × Next.jsのnpmライブラリ選定 [2025年]