state-management について
React開発者向けの状態管理リファレンスです。サーバー、クライアント、フォーム、URLの状態に使うライブラリを示し、再利用可能なパターンを紹介します。
主な機能
- 01サーバー状態に TanStack Query
- 02クライアント状態に Zustand
- 03React Hook Form と Zod のパターン
- 04nuqs または searchParams による URL 状態管理
- 05クエリキーファクトリーとフック
- 06セレクターと状態の永続化
活用シーン
- 01Reactアプリの状態構成
- 02再利用可能なデータ取得フックの作成
- 03コンポーネントの不要な再レンダリングの削減
- 04フォーム状態の検証と管理
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ eddiebe147/ claude-settings --skill state-managementインストール後、Claude Code で/state-managementを入力して読み込まれたか確認します。
詳細
状態管理
考え方
- サーバー状態 → TanStack Query
- クライアント状態 → Zustand
- フォーム状態 → React Hook Form + Zod
- URL 状態 → nuqs または searchParams
TanStack Query
クエリーキーのファクトリー
export const userKeys = {
all: ['users'] as const,
lists: () => [...userKeys.all, 'list'] as const,
list: (filters: Filters) => [...userKeys.lists(), filters] as const,
details: () => [...userKeys.all, 'detail'] as const,
detail: (id: string) => [...userKeys.details(), id] as const,
};
フックのパターン
export function useUsers(filters?: Filters) {
return useQuery({
queryKey: userKeys.list(filters ?? {}),
queryFn: () => getUsers(filters),
});
}
export function useUser(id: string) {
return useQuery({
queryKey: userKeys.detail(id),
queryFn: () => getUser(id),
enabled: !!id,
});
}
export function useCreateUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: createUser,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: userKeys.lists() });
},
});
}
export function useUpdateUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, data }: { id: string; data: UpdateUserInput }) =>
updateUser(id, data),
onSuccess: (_, { id }) => {
queryClient.invalidateQueries({ queryKey: userKeys.detail(id) });
queryClient.invalidateQueries({ queryKey: userKeys.lists() });
},
});
}
Zustand
型付きストア
interface UIStore {
sidebarOpen: boolean;
theme: 'light' | 'dark';
toggleSidebar: () => void;
setTheme: (theme: 'light' | 'dark') => void;
}
export const useUIStore = create<UIStore>((set) => ({
sidebarOpen: true,
theme: 'light',
toggleSidebar: () => set((s) => ({ sidebarOpen: !s.sidebarOpen })),
setTheme: (theme) => set({ theme }),
}));
パフォーマンス:セレクターを使用する
// CORRECT - Only re-renders when sidebarOpen changes
const sidebarOpen = useUIStore((s) => s.sidebarOpen);
// WRONG - Re-renders on ANY state change
const { sidebarOpen } = useUIStore();
永続化ミドルウェア
import { persist } from 'zustand/middleware';
export const useSettingsStore = create<SettingsStore>()(
persist(
(set) => ({
language: 'en',
setLanguage: (language) => set({ language }),
}),
{
name: 'settings-storage',
}
)
);
セレクターによる計算値
// Create a selector
const selectFilteredItems = (state: Store) =>
state.items.filter(item => item.active);
// Use in component
const filteredItems = useStore(selectFilteredItems);
フォーム状態:React Hook Form + Zod
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
const schema = z.object({
name: z.string().min(1, 'Required'),
email: z.string().email('Invalid email'),
});
type FormData = z.infer<typeof schema>;
export function UserForm() {
const { register, handleSubmit, formState: { errors } } = useForm<FormData>({
resolver: zodResolver(schema),
});
const onSubmit = (data: FormData) => {
// data is typed and validated
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('name')} />
{errors.name && <span>{errors.name.message}</span>}
<input {...register('email')} />
{errors.email && <span>{errors.email.message}</span>}
<button type="submit">Submit</button>
</form>
);
}
コメント
Markdown 対応