T-CREATOR

Zustand でネストした状態を扱う:スキーマ設計と更新ロジックの整理術

Zustand でネストした状態を扱う:スキーマ設計と更新ロジックの整理術

Zustand を使った開発では、アプリケーションが複雑になるにつれて、ネストした状態の管理が避けられない課題となります。シンプルなフラット構造から始まったストアも、ユーザー情報、設定項目、フォームデータなどが組み合わさると、多層的な構造へと発展していくでしょう。

しかし、ネストした状態の管理は一筋縄ではいきません。適切な設計なしに進めると、状態の更新が複雑になり、パフォーマンスの問題や型安全性の課題に直面することになります。

本記事では、Zustand でネストした状態を効率的に扱うためのスキーマ設計の考え方と、実用的な更新ロジックの整理術をご紹介します。実際のコード例を交えながら、現場で使える実践的なテクニックを学んでいきましょう。

背景

フラットな状態管理の限界

多くの Zustand プロジェクトは、以下のようなシンプルなフラット構造から始まります。

typescriptinterface AppState {
  count: number;
  user: string;
  theme: string;
  increment: () => void;
  setUser: (name: string) => void;
  setTheme: (theme: string) => void;
}

このアプローチは小規模なアプリケーションでは十分機能しますが、機能が拡張されるにつれて限界が見えてきます。ユーザー情報が名前だけでなく、プロファイル画像、設定、権限なども含むようになると、フラット構造では管理が困難になってしまうのです。

typescript// フラット構造の限界例
interface AppState {
  userName: string;
  userEmail: string;
  userAvatar: string;
  userTheme: string;
  userNotifications: boolean;
  userPrivacy: string;
  // ... 他にも多数のuser関連プロパティ
}

複雑なアプリケーションでのネスト構造の必要性

現代の Web アプリケーションでは、以下のような複雑な状態管理が求められます。

#シーン必要な状態構造
1EC サイト商品カテゴリ、カート内容、ユーザー設定の階層管理
2ダッシュボードウィジェット設定、レイアウト情報、フィルター条件の組み合わせ
3フォームアプリセクション毎の入力値、バリデーション状態、表示制御フラグ

これらのケースでは、関連する状態をグループ化し、階層的に管理することで、コードの可読性と保守性が大幅に向上します。

typescript// ネスト構造での改善例
interface AppState {
  user: {
    profile: {
      name: string;
      email: string;
      avatar: string;
    };
    settings: {
      theme: string;
      notifications: boolean;
      privacy: string;
    };
  };
  cart: {
    items: CartItem[];
    total: number;
  };
}

課題

ネストした状態更新の複雑さ

ネストした状態を扱う際の最大の課題は、更新処理の複雑さです。React の状態更新原則である「イミュータブルな更新」を遵守しながら、深い階層の値を変更するには、多くのボイラープレートコードが必要になります。

typescript// 問題のあるネスト状態更新例
const updateUserTheme = (newTheme: string) => {
  set((state) => ({
    ...state,
    user: {
      ...state.user,
      settings: {
        ...state.user.settings,
        theme: newTheme,
      },
    },
  }));
};

このようなコードは、階層が深くなるほど可読性が悪化し、エラーの温床となります。

パフォーマンスの問題

ネストした状態の不適切な更新は、予期しない再レンダリングを引き起こします。特に、オブジェクト全体を新しく作成してしまうと、参照の変更により関連するコンポーネントすべてが再レンダリングされてしまうのです。

typescript// パフォーマンスに問題のある例
const badUpdate = (userId: string, newName: string) => {
  set((state) => ({
    ...state,
    users: state.users.map((user) =>
      user.id === userId ? { ...user, name: newName } : user
    ),
  }));
};

この例では、特定のユーザーの名前を変更するだけなのに、users 配列全体が新しく作成され、すべてのユーザーコンポーネントが再レンダリングされる可能性があります。

型安全性の確保の困難さ

TypeScript を使用していても、ネストした状態の型安全性を確保するのは簡単ではありません。深い階層でのオプショナルプロパティや、動的に決まるキーを持つオブジェクトの扱いでは、型エラーが発生しやすくなります。

typescript// 型安全性の課題例
interface UserState {
  users: {
    [userId: string]: {
      profile?: {
        name?: string;
        settings?: {
          theme?: string;
        };
      };
    };
  };
}

// このような更新では型エラーが発生しやすい
const updateUserTheme = (userId: string, theme: string) => {
  set((state) => ({
    ...state,
    users: {
      ...state.users,
      [userId]: {
        ...state.users[userId],
        profile: {
          ...state.users[userId]?.profile, // 型エラーの可能性
          settings: {
            ...state.users[userId]?.profile?.settings,
            theme,
          },
        },
      },
    },
  }));
};

解決策

浅い更新 vs 深い更新の使い分け

ネストした状態を効率的に管理するために、まず「浅い更新」と「深い更新」の使い分けを理解することが重要です。

浅い更新は、オブジェクトの第一階層のプロパティのみを変更する方法です。パフォーマンスが良く、予測しやすい動作をしますが、深い階層の変更には向きません。

typescript// 浅い更新の例
interface AppState {
  userProfile: UserProfile;
  appSettings: AppSettings;
  updateUserProfile: (profile: UserProfile) => void;
  updateAppSettings: (settings: AppSettings) => void;
}

const useAppStore = create<AppState>((set) => ({
  userProfile: {
    name: '',
    email: '',
  },
  appSettings: {
    theme: 'light',
    language: 'ja',
  },
  // オブジェクト全体を置き換える浅い更新
  updateUserProfile: (profile) =>
    set({ userProfile: profile }),
  updateAppSettings: (settings) =>
    set({ appSettings: settings }),
}));

深い更新は、ネストした階層の特定のプロパティのみを変更する方法です。より細かい制御が可能ですが、実装が複雑になります。

typescript// 深い更新の例
interface AppState {
  user: {
    profile: {
      name: string;
      email: string;
    };
    settings: {
      theme: string;
      notifications: boolean;
    };
  };
  updateUserName: (name: string) => void;
  updateUserTheme: (theme: string) => void;
}

const useAppStore = create<AppState>((set) => ({
  user: {
    profile: { name: '', email: '' },
    settings: { theme: 'light', notifications: true },
  },
  // 深い更新:名前のみを変更
  updateUserName: (name) =>
    set((state) => ({
      user: {
        ...state.user,
        profile: {
          ...state.user.profile,
          name,
        },
      },
    })),
  // 深い更新:テーマのみを変更
  updateUserTheme: (theme) =>
    set((state) => ({
      user: {
        ...state.user,
        settings: {
          ...state.user.settings,
          theme,
        },
      },
    })),
}));

immer ライブラリとの連携

深い更新の複雑さを解決するために、immer ライブラリとの連携が効果的です。immer を使用すると、ミュータブルな書き方でイミュータブルな更新を実現できます。

まず、必要な依存関係をインストールします。

bashyarn add immer
yarn add -D @types/immer

次に、immer を活用したストアを作成します。

typescriptimport { create } from 'zustand';
import { produce } from 'immer';

interface NestedState {
  user: {
    profile: {
      name: string;
      email: string;
      address: {
        prefecture: string;
        city: string;
        zipCode: string;
      };
    };
    settings: {
      theme: string;
      notifications: {
        email: boolean;
        push: boolean;
        sms: boolean;
      };
    };
  };
  updateUserName: (name: string) => void;
  updateUserCity: (city: string) => void;
  toggleEmailNotification: () => void;
}

const useNestedStore = create<NestedState>((set) => ({
  user: {
    profile: {
      name: '',
      email: '',
      address: {
        prefecture: '',
        city: '',
        zipCode: '',
      },
    },
    settings: {
      theme: 'light',
      notifications: {
        email: true,
        push: false,
        sms: false,
      },
    },
  },
  // immer を使った簡潔な深い更新
  updateUserName: (name) =>
    set(
      produce((state) => {
        state.user.profile.name = name;
      })
    ),
  updateUserCity: (city) =>
    set(
      produce((state) => {
        state.user.profile.address.city = city;
      })
    ),
  toggleEmailNotification: () =>
    set(
      produce((state) => {
        state.user.settings.notifications.email =
          !state.user.settings.notifications.email;
      })
    ),
}));

スライスパターンによる分割管理

大規模なアプリケーションでは、関連する状態と操作をスライス(slice)として分割管理することで、コードの整理と保守性の向上を図れます。

typescript// ユーザー関連のスライス
interface UserSlice {
  user: {
    profile: UserProfile;
    settings: UserSettings;
  };
  updateUserProfile: (
    profile: Partial<UserProfile>
  ) => void;
  updateUserSettings: (
    settings: Partial<UserSettings>
  ) => void;
}

const createUserSlice = (set: any): UserSlice => ({
  user: {
    profile: { name: '', email: '' },
    settings: { theme: 'light', notifications: true },
  },
  updateUserProfile: (profile) =>
    set(
      produce((state: any) => {
        Object.assign(state.user.profile, profile);
      })
    ),
  updateUserSettings: (settings) =>
    set(
      produce((state: any) => {
        Object.assign(state.user.settings, settings);
      })
    ),
});

// ショッピングカート関連のスライス
interface CartSlice {
  cart: {
    items: CartItem[];
    total: number;
  };
  addToCart: (item: CartItem) => void;
  removeFromCart: (itemId: string) => void;
  updateQuantity: (
    itemId: string,
    quantity: number
  ) => void;
}

const createCartSlice = (set: any): CartSlice => ({
  cart: {
    items: [],
    total: 0,
  },
  addToCart: (item) =>
    set(
      produce((state: any) => {
        state.cart.items.push(item);
        state.cart.total += item.price * item.quantity;
      })
    ),
  removeFromCart: (itemId) =>
    set(
      produce((state: any) => {
        const index = state.cart.items.findIndex(
          (item: CartItem) => item.id === itemId
        );
        if (index !== -1) {
          const item = state.cart.items[index];
          state.cart.total -= item.price * item.quantity;
          state.cart.items.splice(index, 1);
        }
      })
    ),
  updateQuantity: (itemId, quantity) =>
    set(
      produce((state: any) => {
        const item = state.cart.items.find(
          (item: CartItem) => item.id === itemId
        );
        if (item) {
          state.cart.total +=
            (quantity - item.quantity) * item.price;
          item.quantity = quantity;
        }
      })
    ),
});

// スライスを結合したメインストア
type AppState = UserSlice & CartSlice;

const useAppStore = create<AppState>((set, get) => ({
  ...createUserSlice(set),
  ...createCartSlice(set),
}));

この分割アプローチにより、各機能の責任が明確になり、テストやデバッグが容易になります。

具体例

ユーザープロファイル管理

実際のユーザープロファイル管理システムを例に、ネストした状態の実装を見てみましょう。

typescriptinterface UserProfile {
  id: string;
  personalInfo: {
    firstName: string;
    lastName: string;
    email: string;
    phone: string;
    birthDate: Date | null;
  };
  address: {
    country: string;
    prefecture: string;
    city: string;
    street: string;
    zipCode: string;
  };
  preferences: {
    language: string;
    timezone: string;
    theme: 'light' | 'dark' | 'auto';
    notifications: {
      email: boolean;
      push: boolean;
      sms: boolean;
    };
  };
  subscription: {
    plan: 'free' | 'premium' | 'enterprise';
    validUntil: Date | null;
    features: string[];
  };
}

interface UserProfileState {
  profile: UserProfile | null;
  isLoading: boolean;
  error: string | null;

  // プロファイル全体の操作
  setProfile: (profile: UserProfile) => void;
  clearProfile: () => void;

  // 個人情報の更新
  updatePersonalInfo: (
    info: Partial<UserProfile['personalInfo']>
  ) => void;

  // 住所情報の更新
  updateAddress: (
    address: Partial<UserProfile['address']>
  ) => void;

  // 設定の更新
  updatePreferences: (
    preferences: Partial<UserProfile['preferences']>
  ) => void;
  updateNotificationSettings: (
    notifications: Partial<
      UserProfile['preferences']['notifications']
    >
  ) => void;

  // サブスクリプション管理
  updateSubscription: (
    subscription: Partial<UserProfile['subscription']>
  ) => void;
}

const useUserProfileStore = create<UserProfileState>(
  (set) => ({
    profile: null,
    isLoading: false,
    error: null,

    setProfile: (profile) => set({ profile, error: null }),
    clearProfile: () => set({ profile: null, error: null }),

    updatePersonalInfo: (info) =>
      set(
        produce((state) => {
          if (state.profile) {
            Object.assign(state.profile.personalInfo, info);
          }
        })
      ),

    updateAddress: (address) =>
      set(
        produce((state) => {
          if (state.profile) {
            Object.assign(state.profile.address, address);
          }
        })
      ),

    updatePreferences: (preferences) =>
      set(
        produce((state) => {
          if (state.profile) {
            Object.assign(
              state.profile.preferences,
              preferences
            );
          }
        })
      ),

    updateNotificationSettings: (notifications) =>
      set(
        produce((state) => {
          if (state.profile) {
            Object.assign(
              state.profile.preferences.notifications,
              notifications
            );
          }
        })
      ),

    updateSubscription: (subscription) =>
      set(
        produce((state) => {
          if (state.profile) {
            Object.assign(
              state.profile.subscription,
              subscription
            );
          }
        })
      ),
  })
);

コンポーネントでの使用例:

typescriptimport React from 'react';
import { useUserProfileStore } from './userProfileStore';

const UserProfileForm: React.FC = () => {
  const {
    profile,
    updatePersonalInfo,
    updateAddress,
    updateNotificationSettings,
  } = useUserProfileStore();

  if (!profile)
    return <div>プロファイルが読み込まれていません</div>;

  return (
    <div>
      <section>
        <h2>個人情報</h2>
        <input
          value={profile.personalInfo.firstName}
          onChange={(e) =>
            updatePersonalInfo({
              firstName: e.target.value,
            })
          }
          placeholder='名前'
        />
        <input
          value={profile.personalInfo.email}
          onChange={(e) =>
            updatePersonalInfo({ email: e.target.value })
          }
          placeholder='メールアドレス'
        />
      </section>

      <section>
        <h2>住所</h2>
        <input
          value={profile.address.prefecture}
          onChange={(e) =>
            updateAddress({ prefecture: e.target.value })
          }
          placeholder='都道府県'
        />
        <input
          value={profile.address.city}
          onChange={(e) =>
            updateAddress({ city: e.target.value })
          }
          placeholder='市区町村'
        />
      </section>

      <section>
        <h2>通知設定</h2>
        <label>
          <input
            type='checkbox'
            checked={
              profile.preferences.notifications.email
            }
            onChange={(e) =>
              updateNotificationSettings({
                email: e.target.checked,
              })
            }
          />
          メール通知
        </label>
        <label>
          <input
            type='checkbox'
            checked={profile.preferences.notifications.push}
            onChange={(e) =>
              updateNotificationSettings({
                push: e.target.checked,
              })
            }
          />
          プッシュ通知
        </label>
      </section>
    </div>
  );
};

ショッピングカート

EC サイトのショッピングカート機能では、商品の追加、削除、数量変更などの複雑な操作が必要です。

typescriptinterface CartItem {
  id: string;
  productId: string;
  name: string;
  price: number;
  quantity: number;
  options: {
    size?: string;
    color?: string;
    customization?: Record<string, any>;
  };
  discount?: {
    type: 'percentage' | 'fixed';
    value: number;
  };
}

interface ShippingInfo {
  method: 'standard' | 'express' | 'overnight';
  cost: number;
  estimatedDays: number;
}

interface CartState {
  items: CartItem[];
  shipping: ShippingInfo | null;
  promoCode: string | null;
  totals: {
    subtotal: number;
    discount: number;
    shipping: number;
    tax: number;
    total: number;
  };

  // カート操作
  addItem: (item: Omit<CartItem, 'id'>) => void;
  removeItem: (itemId: string) => void;
  updateQuantity: (
    itemId: string,
    quantity: number
  ) => void;
  updateItemOptions: (
    itemId: string,
    options: Partial<CartItem['options']>
  ) => void;
  clearCart: () => void;

  // 配送・決済
  setShipping: (shipping: ShippingInfo) => void;
  applyPromoCode: (code: string) => void;
  removePromoCode: () => void;

  // 計算
  calculateTotals: () => void;
}

const useCartStore = create<CartState>((set, get) => ({
  items: [],
  shipping: null,
  promoCode: null,
  totals: {
    subtotal: 0,
    discount: 0,
    shipping: 0,
    tax: 0,
    total: 0,
  },

  addItem: (newItem) =>
    set(
      produce((state) => {
        // 既存のアイテムかチェック(商品ID + オプションで判定)
        const existingItemIndex = state.items.findIndex(
          (item) =>
            item.productId === newItem.productId &&
            JSON.stringify(item.options) ===
              JSON.stringify(newItem.options)
        );

        if (existingItemIndex >= 0) {
          // 既存アイテムの数量を増加
          state.items[existingItemIndex].quantity +=
            newItem.quantity;
        } else {
          // 新規アイテムを追加
          state.items.push({
            ...newItem,
            id: `${newItem.productId}-${Date.now()}`,
          });
        }
      })
    ),

  removeItem: (itemId) =>
    set(
      produce((state) => {
        const index = state.items.findIndex(
          (item) => item.id === itemId
        );
        if (index >= 0) {
          state.items.splice(index, 1);
        }
      })
    ),

  updateQuantity: (itemId, quantity) =>
    set(
      produce((state) => {
        const item = state.items.find(
          (item) => item.id === itemId
        );
        if (item && quantity > 0) {
          item.quantity = quantity;
        }
      })
    ),

  updateItemOptions: (itemId, options) =>
    set(
      produce((state) => {
        const item = state.items.find(
          (item) => item.id === itemId
        );
        if (item) {
          Object.assign(item.options, options);
        }
      })
    ),

  clearCart: () =>
    set({
      items: [],
      promoCode: null,
      totals: {
        subtotal: 0,
        discount: 0,
        shipping: 0,
        tax: 0,
        total: 0,
      },
    }),

  setShipping: (shipping) => set({ shipping }),

  applyPromoCode: (code) => set({ promoCode: code }),

  removePromoCode: () => set({ promoCode: null }),

  calculateTotals: () =>
    set(
      produce((state) => {
        // 小計の計算
        state.totals.subtotal = state.items.reduce(
          (sum, item) => {
            let itemTotal = item.price * item.quantity;

            // アイテム固有の割引を適用
            if (item.discount) {
              if (item.discount.type === 'percentage') {
                itemTotal *=
                  (100 - item.discount.value) / 100;
              } else {
                itemTotal -= item.discount.value;
              }
            }

            return sum + itemTotal;
          },
          0
        );

        // プロモコードによる割引(簡略化)
        state.totals.discount = state.promoCode
          ? state.totals.subtotal * 0.1
          : 0;

        // 配送料
        state.totals.shipping = state.shipping?.cost || 0;

        // 税金(消費税10%として計算)
        const taxableAmount =
          state.totals.subtotal -
          state.totals.discount +
          state.totals.shipping;
        state.totals.tax = taxableAmount * 0.1;

        // 合計
        state.totals.total =
          state.totals.subtotal -
          state.totals.discount +
          state.totals.shipping +
          state.totals.tax;
      })
    ),
}));

フォーム状態管理

複雑なフォームでは、入力値、バリデーション状態、表示制御など多様な状態を管理する必要があります。

typescriptinterface FieldState {
  value: any;
  error: string | null;
  touched: boolean;
  dirty: boolean;
}

interface FormSection {
  fields: Record<string, FieldState>;
  isValid: boolean;
  isVisible: boolean;
}

interface MultiStepFormState {
  currentStep: number;
  sections: {
    personal: FormSection;
    contact: FormSection;
    preferences: FormSection;
    confirmation: FormSection;
  };

  // フォーム操作
  nextStep: () => void;
  prevStep: () => void;
  goToStep: (step: number) => void;

  // フィールド操作
  setFieldValue: (
    section: keyof MultiStepFormState['sections'],
    fieldName: string,
    value: any
  ) => void;
  setFieldError: (
    section: keyof MultiStepFormState['sections'],
    fieldName: string,
    error: string | null
  ) => void;
  touchField: (
    section: keyof MultiStepFormState['sections'],
    fieldName: string
  ) => void;

  // セクション操作
  validateSection: (
    section: keyof MultiStepFormState['sections']
  ) => boolean;
  resetSection: (
    section: keyof MultiStepFormState['sections']
  ) => void;

  // フォーム全体
  validateForm: () => boolean;
  resetForm: () => void;
  submitForm: () => Promise<void>;
}

const initialFieldState: FieldState = {
  value: '',
  error: null,
  touched: false,
  dirty: false,
};

const createFormSection = (
  fields: string[]
): FormSection => ({
  fields: fields.reduce((acc, fieldName) => {
    acc[fieldName] = { ...initialFieldState };
    return acc;
  }, {} as Record<string, FieldState>),
  isValid: false,
  isVisible: true,
});

const useMultiStepFormStore = create<MultiStepFormState>(
  (set, get) => ({
    currentStep: 0,
    sections: {
      personal: createFormSection([
        'firstName',
        'lastName',
        'birthDate',
      ]),
      contact: createFormSection([
        'email',
        'phone',
        'address',
      ]),
      preferences: createFormSection([
        'theme',
        'notifications',
        'language',
      ]),
      confirmation: createFormSection([]),
    },

    nextStep: () =>
      set((state) => ({
        currentStep: Math.min(
          state.currentStep + 1,
          Object.keys(state.sections).length - 1
        ),
      })),

    prevStep: () =>
      set((state) => ({
        currentStep: Math.max(state.currentStep - 1, 0),
      })),

    goToStep: (step) =>
      set((state) => ({
        currentStep: Math.max(
          0,
          Math.min(
            step,
            Object.keys(state.sections).length - 1
          )
        ),
      })),

    setFieldValue: (sectionName, fieldName, value) =>
      set(
        produce((state) => {
          const field =
            state.sections[sectionName].fields[fieldName];
          if (field) {
            field.value = value;
            field.dirty = true;
            field.error = null; // 値が変更されたらエラーをクリア
          }
        })
      ),

    setFieldError: (sectionName, fieldName, error) =>
      set(
        produce((state) => {
          const field =
            state.sections[sectionName].fields[fieldName];
          if (field) {
            field.error = error;
          }
        })
      ),

    touchField: (sectionName, fieldName) =>
      set(
        produce((state) => {
          const field =
            state.sections[sectionName].fields[fieldName];
          if (field) {
            field.touched = true;
          }
        })
      ),

    validateSection: (sectionName) => {
      const { sections } = get();
      const section = sections[sectionName];

      // 簡単なバリデーション例
      const isValid = Object.values(section.fields).every(
        (field) =>
          field.value !== '' && field.error === null
      );

      set(
        produce((state) => {
          state.sections[sectionName].isValid = isValid;
        })
      );

      return isValid;
    },

    resetSection: (sectionName) =>
      set(
        produce((state) => {
          const section = state.sections[sectionName];
          Object.keys(section.fields).forEach(
            (fieldName) => {
              section.fields[fieldName] = {
                ...initialFieldState,
              };
            }
          );
          section.isValid = false;
        })
      ),

    validateForm: () => {
      const { sections } = get();
      return Object.keys(sections).every((sectionName) =>
        get().validateSection(
          sectionName as keyof MultiStepFormState['sections']
        )
      );
    },

    resetForm: () =>
      set((state) => ({
        currentStep: 0,
        sections: {
          personal: createFormSection([
            'firstName',
            'lastName',
            'birthDate',
          ]),
          contact: createFormSection([
            'email',
            'phone',
            'address',
          ]),
          preferences: createFormSection([
            'theme',
            'notifications',
            'language',
          ]),
          confirmation: createFormSection([]),
        },
      })),

    submitForm: async () => {
      const isValid = get().validateForm();
      if (!isValid) {
        throw new Error('フォームに入力エラーがあります');
      }

      // 実際の送信処理
      try {
        const formData = get().sections;
        // API呼び出しなど
        console.log('フォームデータを送信:', formData);
      } catch (error) {
        console.error('送信エラー:', error);
        throw error;
      }
    },
  })
);

コンポーネントでの使用例:

typescriptimport React from 'react';
import { useMultiStepFormStore } from './multiStepFormStore';

const PersonalInfoStep: React.FC = () => {
  const {
    sections,
    setFieldValue,
    touchField,
    validateSection,
  } = useMultiStepFormStore();

  const personalSection = sections.personal;

  const handleFieldChange = (
    fieldName: string,
    value: string
  ) => {
    setFieldValue('personal', fieldName, value);
  };

  const handleFieldBlur = (fieldName: string) => {
    touchField('personal', fieldName);
    validateSection('personal');
  };

  return (
    <div>
      <h2>個人情報の入力</h2>

      <div>
        <label>名前</label>
        <input
          value={
            personalSection.fields.firstName?.value || ''
          }
          onChange={(e) =>
            handleFieldChange('firstName', e.target.value)
          }
          onBlur={() => handleFieldBlur('firstName')}
        />
        {personalSection.fields.firstName?.error && (
          <span className='error'>
            {personalSection.fields.firstName.error}
          </span>
        )}
      </div>

      <div>
        <label></label>
        <input
          value={
            personalSection.fields.lastName?.value || ''
          }
          onChange={(e) =>
            handleFieldChange('lastName', e.target.value)
          }
          onBlur={() => handleFieldBlur('lastName')}
        />
        {personalSection.fields.lastName?.error && (
          <span className='error'>
            {personalSection.fields.lastName.error}
          </span>
        )}
      </div>
    </div>
  );
};

まとめ

Zustand でネストした状態を効率的に管理するためには、適切な設計とツールの活用が欠かせません。本記事でご紹介した手法をまとめると、以下のようになります。

設計の原則として、浅い更新と深い更新の使い分けを理解し、パフォーマンスと可読性のバランスを取ることが重要です。単純な状態変更には浅い更新を、複雑な階層構造には immer を活用した深い更新を選択しましょう。

実装のテクニックでは、immer ライブラリとの連携により、ミュータブルな書き方でイミュータブルな更新を実現できます。また、スライスパターンによる分割管理で、大規模なアプリケーションでもコードの整理と保守性を保てるのです。

型安全性の確保については、TypeScript との組み合わせで、コンパイル時にエラーを検出し、開発効率を向上させることができます。

実際のプロジェクトでは、アプリケーションの規模と複雑さに応じて、これらの手法を組み合わせて使用することになるでしょう。小さく始めて、必要に応じて段階的に高度なパターンを導入していくアプローチをお勧めします。

ネストした状態管理は一見複雑に見えますが、適切な設計と実装パターンを身につけることで、保守性の高い状態管理システムを構築できるようになります。ぜひ、本記事の内容を参考に、実際のプロジェクトで活用してみてください。

関連リンク