本記事では、シンプルで保守性の高い状態管理アーキテクチャを実現するための実践的なガイドを紹介します。
用途としては、「Vibe Coding による MVP 開発」を想定しているため、下記を重視します。
- シンプルさ
- AI フレンドリー
- 複雑な要件は実装しない
- 高度なパフォーマンス最適化は必要としない
一言で言えば、dApps の MVP では zustand + react-hook-form で十分かつ最適ではないかと。
基本方針
技術選定の原則
状態管理において重視するのが、技術構成をシンプルに保つことです。
- グローバル状態管理: Zustand のみ使用
- フォーム状態管理: React Hook Form のみ使用
- 他の状態管理ライブラリとの併用禁止: Context, Redux、Jotai、Valtio 等は使用しない
- 保守性: 技術構成をシンプルに保ち、学習コストを最小化
状態管理の責任分担
下記のように使い分けます。
状態の種類 | 管理方法 | 用途例 |
---|---|---|
グローバル状態 | Zustand | ユーザー情報、Wallet 接続状態、通知 |
フォーム状態 | React Hook Form | フォーム入力、バリデーション |
サーバー状態 | TanStack Query | API データ(将来実装) |
Zustand による効率的な状態管理
ドメイン別ストア分割パターン
大規模アプリケーションでは、機能・ドメイン別にストアを分割することが重要です。
// 機能・ドメイン別に分割(dAppsの例)
src/stores/
├── mintStore.ts // Mint機能専用
├── walletStore.ts // Wallet接続専用
├── uiStore.ts // UI状態専用
└── nftStore.ts // NFT データ専用
Store 構造テンプレート
以下は、dApps における構造テンプレート例です。
// 実装例(mintStore.ts より)
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
interface MintStore {
// State
mintingState: 'idle' | 'minting' | 'success' | 'error';
mintedNftId: string | null;
errorMessage: string | null;
// Actions
mintAgent: () => Promise<void>;
resetMintState: () => void;
updateConfig: (config: Partial<AgentConfig>) => void;
}
export const useMintStore = create<MintStore>()(
devtools(
(set, get) => ({
// Initial state
mintingState: 'idle',
mintedNftId: null,
errorMessage: null,
// Actions
mintAgent: async () => {
// 実装
},
resetMintState: () => {
set({
mintingState: 'idle',
mintedNftId: null,
errorMessage: null,
});
},
updateConfig: (config) => {
set((state) => ({
selectedStrategy: config.strategy ?? state.selectedStrategy,
// ...
}));
},
}),
{
name: 'mint-store', // DevTools 表示名
}
)
);
永続化の適切な使用
永続化は必要最小限に留めることが重要です。
// UI設定等、永続化が必要な状態のみ persist middleware を使用
import { persist } from 'zustand/middleware';
export const useUIStore = create<UIStore>()(
devtools(
persist(
(set, get) => ({
// ...store implementation
}),
{
name: 'ui-store',
partialize: (state) => ({
// 永続化する項目のみ指定
sidebarOpen: state.sidebarOpen,
theme: state.theme,
}),
}
),
{ name: 'ui-store' }
)
);
非同期処理とエラーハンドリング
堅牢な非同期 Action の実装
ブロックチェーン操作など、失敗の可能性が高い処理では、適切なエラーハンドリングが不可欠です。
// エラーハンドリングとローディング状態管理
mintAgent: async (signAndExecuteTransaction?: any) => {
const { selectedStrategy, mintPrice, isWalletConnected } = get();
if (!isWalletConnected) {
set({
errorMessage: "Please connect your wallet first",
mintingState: "error",
});
return;
}
try {
set({ mintingState: "minting", errorMessage: null });
const result = await executeMintTransaction({
strategy: selectedStrategy,
mintPrice,
});
if (result.success) {
set({
mintingState: "success",
mintedNftId: result.nftObjectId,
transactionHash: result.transactionDigest,
});
} else {
throw new Error(result.error || "Transaction failed");
}
} catch (error) {
set({
mintingState: "error",
errorMessage: error instanceof Error ? error.message : "Failed to mint",
});
}
},
指数バックオフ付きリトライ機能
ネットワーク障害に対する堅牢性を向上させるため、指数バックオフ付きリトライを実装します。
// 指数バックオフ付きリトライ
retryMint: async (signAndExecuteTransaction?: any) => {
const { retryCount, maxRetries } = get();
if (retryCount >= maxRetries) {
set({
errorMessage: `Maximum retry attempts (${maxRetries}) 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));
set({ errorMessage: null });
await get().mintAgent(signAndExecuteTransaction);
},
React Hook Form との統合パターン
Custom Hook による橋渡し
Zustand と React Hook Form の間に Custom Hook を挟むことで、関心の分離を実現します。
// useMintForm.ts - Zustand と react-hook-form の橋渡し
import { useEffect } from 'react';
import { useMintStore } from '@/stores/mintStore';
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 });
// 戦略選択時に自動的にデフォルト価格を設定
if (field === 'strategy' && value) {
const strategy = strategyOptions.find((s) => s.value === value);
if (strategy?.defaultMintPrice) {
updateConfig({ mintPrice: strategy.defaultMintPrice });
}
}
};
const isFormValid = selectedStrategy && mintPrice && isWalletConnected;
return {
selectedStrategy,
mintPrice,
isFormValid,
handleConfigChange,
};
};
このパターンにより、フォームロジックとグローバル状態管理を綺麗に分離できます。
通知システムの実装
グローバル通知管理
ユーザーへの適切なフィードバックのため、グローバル通知システムを実装します。
// uiStore.ts - 通知システム
addNotification: (notification) => {
const id = `notification-${Date.now()}-${Math.random()}`;
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);
}
},
パフォーマンス最適化
Selector パターンの活用
不要な再レンダリングを防ぐため、Selector パターンを活用します。
// 必要な状態のみを購読
const useMintingStatus = () =>
useMintStore((state) => ({
mintingState: state.mintingState,
errorMessage: state.errorMessage,
}));
// ❌ 悪い例:Store全体を購読(不要な再レンダリング)
const store = useMintStore();
React.memo による最適化
// React.memo でコンポーネント最適化
import { memo } from 'react';
export const WalletStatus = memo(() => {
const isConnected = useWalletStore((state) => state.isConnected);
return <div>{isConnected ? 'Connected' : 'Disconnected'}</div>;
});
WalletStatus.displayName = 'WalletStatus';
共通エラーハンドリングパターン
統一されたエラー処理
複数の Action で共通のエラーハンドリングロジックを使用します。
// 共通エラーハンドリング関数
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: 開発時の devtools middleware で生産性向上
- Persistence: 必要最小限の状態のみ永続化
- Custom Hooks: Zustand と react-hook-form の橋渡し
- Selector: 必要な状態のみ購読で最適化
- エラーハンドリング: 統一されたエラー処理パターン
避けるべきパターン
- 複数ライブラリ併用: Context, Redux、Jotai、Valtio 等との併用は複雑性を増す
- Store 肥大化: 1 つの Store に全機能を詰め込むのは保守性を損なう
- 直接操作: set()の外部からの直接呼び出しは予期しない副作用を生む
- 過度な永続化: 全状態を localStorage に保存するのはパフォーマンスを悪化させる