mcpField
S
state-management

state-management

github.com/mgd34msu開発671 回インストール11 週間前に更新
リポジトリ

state-management について

TypeScript アプリの状態管理を計画・実装するためのスキルです。サーバー、クライアント、フォーム、コンポーネント、URL の状態に適した方法を選び、実装を検証できます。

主な機能

  1. 01既存の状態ライブラリの調査
  2. 02状態の種類に応じた選定ガイド
  3. 03TanStack Query のパターン
  4. 04Zustand ストアのパターン
  5. 05React Hook Form と Zod のフォーム
  6. 06URL 状態の実装と検証

活用シーン

  1. 01既存コードベースで状態管理ツールを選ぶ
  2. 02キャッシュ付き API データと変更処理を追加する
  3. 03複雑な検証付きフォームを構築する
  4. 04状態管理方式を移行して実装を確認する

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/mgd34msu/goodvibes-plugin --skill state-management

インストール後、Claude Code で/state-managementを入力して読み込まれたか確認します。

同梱ファイルreferences/state-patterns.mdscripts/validate-state.sh

詳細

リソース

scripts/
  validate-state.sh
references/
  state-patterns.md

状態管理

このスキルでは、GoodVibes の精密ツールを使用して、状態アーキテクチャの選択と実装を進めます。状態管理ソリューションの選択、データ取得パターンの実装、複雑なフォーム状態の管理を行う際に、このワークフローを使用してください。

このスキルを使用するタイミング

次の場合に、このスキルを読み込んでください。

  • 状態管理のアプローチを選択するとき
  • キャッシュ機能を備えたサーバーデータ取得を実装するとき
  • 複雑なバリデーションを伴うフォームを構築するとき
  • クライアント側のアプリケーション状態を管理するとき
  • URL ベースの状態パターンを実装するとき
  • ある状態管理ソリューションから別のものへ移行するとき

トリガーフレーズ:「状態管理」、「TanStack Query」、「Zustand」、「React Hook Form」、「フォームバリデーション」、「データ取得」、「キャッシュの無効化」、「URL 状態」。

基本的なワークフロー

フェーズ 1:調査

状態管理ソリューションを選択する前に、既存のパターンを把握します。

ステップ 1.1:現在の状態管理ライブラリを特定する

discover を使用して、既存の状態管理ソリューションを見つけます。

discover:
  queries:
    - id: state_libraries
      type: grep
      pattern: "(from 'zustand'|from '@tanstack/react-query'|from 'react-hook-form'|from 'jotai'|from 'redux'|useContext)"
      glob: "**/*.{ts,tsx}"
    - id: data_fetching
      type: grep
      pattern: "(useQuery|useMutation|useSWR|fetch|axios)"
      glob: "**/*.{ts,tsx}"
    - id: form_libraries
      type: grep
      pattern: "(useForm|Formik|react-hook-form)"
      glob: "**/*.{ts,tsx}"
  verbosity: files_only

確認できる内容:

  • 使用中の既存の状態管理ライブラリ
  • データ取得パターン(REST、GraphQL など)
  • フォーム処理のアプローチ
  • コードベース全体の一貫性
ステップ 1.2:パッケージの依存関係を分析する

precision_read を使用して、インストール済みのものを確認します。

precision_read:
  files:
    - path: "package.json"
      extract: content
  verbosity: minimal

次を確認します。

  • @tanstack/react-query(サーバー状態)
  • zustand(クライアント状態)
  • react-hook-form + zod(フォーム)
  • nuqs など(URL 状態)
ステップ 1.3:既存の状態管理パターンを読む

実装パターンを把握するため、2~3 個の例を読みます。

precision_read:
  files:
    - path: "src/lib/query-client.ts"  # or discovered file
      extract: content
    - path: "src/stores/user-store.ts"  # or discovered file
      extract: content
  verbosity: standard
フェーズ 2:状態の分類

状態の種類ごとに適切なツールを選択します。完全な判断ツリーについては、references/state-patterns.md を参照してください。

簡易判断ガイド
状態の種類最適なツール使用する場面
サーバー状態TanStack QueryAPI からのデータで、キャッシュや無効化が必要な場合
クライアント状態Zustandコンポーネント間で共有する UI 状態
フォーム状態React Hook Form + Zodバリデーションを伴う複雑なフォーム
URL 状態nuqs または searchParams共有やブックマークが可能な状態
コンポーネント状態useState1 つのコンポーネント内に限定される場合

状態のコロケーション原則: 状態は、使用する場所にできるだけ近い位置に保ちます。まず useState を使い、共有が必要になったら親コンポーネントに移し、その後、必要な場合にのみ専用のソリューションを検討してください。

フェーズ 3:TanStack Query によるサーバー状態の管理

キャッシュ、バックグラウンド更新、楽観的ミューテーションが必要な API データに使用します。

ステップ 3.1:依存関係をインストールする

インストール済みか確認し、そうでなければ追加します。

npm install @tanstack/react-query  # Note: Targeting TanStack Query v5
npm install -D @tanstack/react-query-devtools
ステップ 3.2:Query Client を設定する

Query Client の設定を作成します。

// src/lib/query-client.ts
import { QueryClient } from '@tanstack/react-query';

export const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 1000 * 60, // 1 minute
      retry: 1,
      refetchOnWindowFocus: false,
    },
  },
});
ステップ 3.3:クエリパターンを実装する

基本的なクエリ:

import { useQuery } from '@tanstack/react-query';
import { getUser } from '@/lib/api';

export function useUser(userId: string) {
  return useQuery({
    queryKey: ['user', userId],
    queryFn: () => getUser(userId),
    enabled: !!userId, // Don't run if no userId
  });
}

楽観的更新を伴うミューテーション:

import { useMutation, useQueryClient } from '@tanstack/react-query';
import { updateUser } from '@/lib/api';

export function useUpdateUser() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: updateUser,
    onMutate: async (newUser) => {
      // Cancel outgoing queries
      await queryClient.cancelQueries({ queryKey: ['user', newUser.id] });

      // Snapshot previous value
      const previousUser = queryClient.getQueryData(['user', newUser.id]);

      // Optimistically update
      queryClient.setQueryData(['user', newUser.id], newUser);

      return { previousUser };
    },
    onError: (err, newUser, context) => {
      // Rollback on error
      queryClient.setQueryData(
        ['user', newUser.id],
        context?.previousUser
      );
    },
    onSettled: (data, error, variables) => {
      // Refetch after error or success
      queryClient.invalidateQueries({ queryKey: ['user', variables.id] });
    },
  });
}

キャッシュの無効化:

// Invalidate all user queries
queryClient.invalidateQueries({ queryKey: ['user'] });

// Invalidate specific user
queryClient.invalidateQueries({ queryKey: ['user', userId] });

// Remove from cache entirely
queryClient.removeQueries({ queryKey: ['user', userId] });

エラー状態と読み込み状態の利用:

function UserProfile({ userId }: { userId: string }) {
  const { data, isPending, isError, error } = useUser(userId);

  if (isPending) return <Skeleton />;
  if (isError) return <ErrorDisplay error={error} />;
  
  return <UserProfile user={data} />;
}
フェーズ 4:Zustand によるクライアント状態の管理

コンポーネント間で共有する UI 状態(モーダル、テーマ、フィルター)に使用します。

ステップ 4.1:依存関係をインストールする
npm install zustand
ステップ 4.2:ストアを作成する

シンプルなストア:

import { create } from 'zustand';

interface UIStore {
  sidebarOpen: boolean;
  toggleSidebar: () => void;
  theme: 'light' | 'dark';
  setTheme: (theme: 'light' | 'dark') => void;
}

export const useUIStore = create<UIStore>((set) => ({
  sidebarOpen: true,
  toggleSidebar: () => set((state) => ({ sidebarOpen: !state.sidebarOpen })),
  theme: 'light',
  setTheme: (theme) => set({ theme }),
}));

スライスを使ったストア:

import { create, StateCreator } from 'zustand';
import { devtools, persist } from 'zustand/middleware';

// Slice pattern for organization
interface AuthSlice {
  user: User | null;
  setUser: (user: User | null) => void;
}

interface UISlice {
  sidebarOpen: boolean;
  toggleSidebar: () => void;
}

type Store = AuthSlice & UISlice;

const createAuthSlice: StateCreator<Store, [], [], AuthSlice> = (set) => ({
  user: null,
  setUser: (user) => set({ user }),
});

const createUISlice: StateCreator<Store, [], [], UISlice> = (set) => ({
  sidebarOpen: true,
  toggleSidebar: () => set((state: Store) => ({ 
    sidebarOpen: !state.sidebarOpen 
  })),
});

export const useStore = create<Store>()(
  devtools(
    persist(
      (...a) => ({
        ...createAuthSlice(...a),
        ...createUISlice(...a),
      }),
      { name: 'app-store' }
    )
  )
);

セレクターの使用:

// Avoid re-renders by selecting only what you need
const sidebarOpen = useUIStore((state) => state.sidebarOpen);
const toggleSidebar = useUIStore((state) => state.toggleSidebar);
フェーズ 5:React Hook Form + Zod によるフォーム状態の管理

バリデーション、フィールド配列、ネストされたオブジェクトを含む複雑なフォームに使用します。

ステップ 5.1:依存関係をインストールする
npm install react-hook-form zod @hookform/resolvers
ステップ 5.2:バリデーションスキーマを定義する
import { z } from 'zod';

export const userSchema = z.object({
  email: z.string().email('Invalid email address'),
  name: z.string().min(2, 'Name must be at least 2 characters'),
  age: z.number().min(18, 'Must be 18 or older'),
  role: z.enum(['user', 'admin']).default('user'),
  preferences: z.object({
    newsletter: z.boolean().default(false),
    notifications: z.boolean().default(true),
  }),
});

export type UserFormData = z.infer<typeof userSchema>;
ステップ 5.3:フォームを実装する

基本的なフォーム:

import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { userSchema, type UserFormData } from './schema';

export function UserForm() {
  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting },
  } = useForm<UserFormData>({
    resolver: zodResolver(userSchema),
    defaultValues: {
      role: 'user',
      preferences: {
        newsletter: false,
        notifications: true,
      },
    },
  });

  const onSubmit = async (data: UserFormData) => {
    await createUser(data);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register('email')} />
      {errors.email && <span>{errors.email.message}</span>}

      <input {...register('name')} />
      {errors.name && <span>{errors.name.message}</span>}

      <input type="number" {...register('age', { valueAsNumber: true })} />
      {errors.age && <span>{errors.age.message}</span>}

      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? 'Submitting...' : 'Submit'}
      </button>
    </form>
  );
}

フィールド配列:

import { useFieldArray } from 'react-hook-form';

const schema = z.object({
  users: z.array(
    z.object({
      name: z.string().min(1),
      email: z.string().email(),
    })
  ).min(1, 'At least one user required'),
});

function UsersForm() {
  const { control, register } = useForm({
    resolver: zodResolver(schema),
  });

  const { fields, append, remove } = useFieldArray({
    control,
    name: 'users',
  });

  return (
    <div>
      {fields.map((field, index) => (
        <div key={field.id}>
          <input {...register(`users.${index}.name`)} />
          <input {...register(`users.${index}.email`)} />
          <button type="button" onClick={() => remove(index)}>
            Remove
          </button>
        </div>
      ))}
      <button
        type="button"
        onClick={() => append({ name: '', email: '' })}
      >
        Add User
      </button>
    </div>
  );
}
フェーズ 6:URL 状態パターン

共有やブックマークが可能な状態に使用します。

ステップ 6.1:nuqs を使用する(推奨)
npm install nuqs  # Note: Targeting nuqs v1.x
import { useQueryState, parseAsInteger, parseAsStringEnum } from 'nuqs';

export function ProductList() {
  const [page, setPage] = useQueryState(
    'page',
    parseAsInteger.withDefault(1)
  );

  const [sort, setSort] = useQueryState(
    'sort',
    parseAsStringEnum(['name', 'price', 'date']).withDefault('name')
  );

  // URL: /products?page=2&sort=price
  // Automatically synced, type-safe, bookmarkable
}
ステップ 6.2:Next.js の searchParams を使用する
import { useSearchParams, useRouter } from 'next/navigation';

export function ProductList() {
  const searchParams = useSearchParams();
  const router = useRouter();

  const page = Number(searchParams.get('page')) || 1;

  const setPage = (newPage: number) => {
    const params = new URLSearchParams(searchParams);
    params.set('page', String(newPage));
    router.push(`?${params.toString()}`);
  };
}
フェーズ 7:精密ツールを使った実装

precision_write を使用して、状態管理ファイルを作成します。

precision_write:
  files:
    - path: "src/lib/query-client.ts"
      content: |
        import { QueryClient } from '@tanstack/react-query';
        export const queryClient = new QueryClient({ ... });
    - path: "src/stores/ui-store.ts"
      content: |
        import { create } from 'zustand';
        export const useUIStore = create({ ... });
    - path: "src/schemas/user-schema.ts"
      content: |
        import { z } from 'zod';
        export const userSchema = z.object({ ... });
  verbosity: count_only
フェーズ 8:検証
ステップ 8.1:検証スクリプトを実行する

検証スクリプトを使用して、品質を確保します。

bash scripts/validate-state.sh .

検証スイートの全容については、scripts/validate-state.sh を参照してください。

ステップ 8.2:型チェック

TypeScript のコンパイルを確認します。

precision_exec:
  commands:
    - cmd: "npm run typecheck"
      expect:
        exit_code: 0
  verbosity: minimal

よくあるパターン

パターン 1:TanStack Query + Zustand の組み合わせ
// Server data with TanStack Query
const { data: user } = useQuery({ queryKey: ['user'], queryFn: getUser });

// UI state with Zustand
const { sidebarOpen, toggleSidebar } = useUIStore();
パターン 2:サーバーのミューテーションを伴うフォーム
const mutation = useMutation({
  mutationFn: createUser,
  onSuccess: () => {
    queryClient.invalidateQueries({ queryKey: ['users'] });
  },
});

const onSubmit = (data: UserFormData) => {
  mutation.mutate(data);
};
パターン 3:データ取得を駆動する URL の状態
const [page] = useQueryState('page', parseAsInteger.withDefault(1));
const [search] = useQueryState('search');

const { data } = useQuery({
  queryKey: ['products', page, search],
  queryFn: () => getProducts({ page, search }),
});

よくあるアンチパターン

避けること:

  • サーバーデータにグローバル状態を使用する(TanStack Query を使用してください)
  • すべてを Zustand に入れる(可能な場合は状態を近くに配置してください)
  • フォームの状態を手動で管理する(React Hook Form を使用してください)
  • UI の状態を URL パラメーターに保存する(Zustand を使用してください)
  • 頻繁に変化するデータに Context を使用する(再レンダリングの原因になります)
  • ミューテーション後にキャッシュを無効化し忘れる
  • フォームのバリデーションスキーマを省略する
  • 状態ストアで any 型を使用する

実施すること:

  • 状態の種類に適したツールを選ぶ
  • 可能な場合は状態を近くに配置する
  • セレクターを使用して再レンダリングを防ぐ
  • UX 向上のために楽観的更新を実装する
  • すべてのフォーム入力を Zod で検証する
  • すべての状態定義に TypeScript を使用する
  • ミューテーション後にクエリを無効化する
  • クエリーキーの一貫性を保つ

クイックリファレンス

発見フェーズ:

discover: { queries: [state_libraries, data_fetching, forms], verbosity: files_only }
precision_read: { files: ["package.json", example stores], extract: content }

実装フェーズ:

precision_write: { files: [query-client, stores, schemas], verbosity: count_only }

検証フェーズ:

precision_exec: { commands: [{ cmd: "npm run typecheck" }], verbosity: minimal }

実装後:

bash scripts/validate-state.sh .

詳細なパターンと判断ツリーについては、references/state-patterns.md を参照してください。

コメント