state-management について
TypeScript アプリの状態管理を計画・実装するためのスキルです。サーバー、クライアント、フォーム、コンポーネント、URL の状態に適した方法を選び、実装を検証できます。
主な機能
- 01既存の状態ライブラリの調査
- 02状態の種類に応じた選定ガイド
- 03TanStack Query のパターン
- 04Zustand ストアのパターン
- 05React Hook Form と Zod のフォーム
- 06URL 状態の実装と検証
活用シーン
- 01既存コードベースで状態管理ツールを選ぶ
- 02キャッシュ付き API データと変更処理を追加する
- 03複雑な検証付きフォームを構築する
- 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 Query | API からのデータで、キャッシュや無効化が必要な場合 |
| クライアント状態 | Zustand | コンポーネント間で共有する UI 状態 |
| フォーム状態 | React Hook Form + Zod | バリデーションを伴う複雑なフォーム |
| URL 状態 | nuqs または searchParams | 共有やブックマークが可能な状態 |
| コンポーネント状態 | useState | 1 つのコンポーネント内に限定される場合 |
状態のコロケーション原則:
状態は、使用する場所にできるだけ近い位置に保ちます。まず 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 を参照してください。