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

本記事では、シンプルで保守性の高い状態管理アーキテクチャを実現するための実践的なガイドを紹介します。
用途としては、「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,
    });
  }
};

まとめ

推奨パターン

  1. Store 分割: 機能・ドメイン別に分割し、責任を明確化
  2. DevTools: 開発時の devtools middleware で生産性向上
  3. Persistence: 必要最小限の状態のみ永続化
  4. Custom Hooks: Zustand と react-hook-form の橋渡し
  5. Selector: 必要な状態のみ購読で最適化
  6. エラーハンドリング: 統一されたエラー処理パターン

避けるべきパターン

  1. 複数ライブラリ併用: Context, Redux、Jotai、Valtio 等との併用は複雑性を増す
  2. Store 肥大化: 1 つの Store に全機能を詰め込むのは保守性を損なう
  3. 直接操作: set()の外部からの直接呼び出しは予期しない副作用を生む
  4. 過度な永続化: 全状態を localStorage に保存するのはパフォーマンスを悪化させる
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?