state-management について
TanStack Query v5 と Zustand v5 を使う React 開発者やコーディングエージェント向けのリファレンスです。サーバー・クライアント状態、キャッシュ、更新処理、安全な永続化を扱います。
主な機能
- 01TanStack Query v5 のパターン
- 02Zustand v5 のストアパターン
- 03キャッシュと無効化の指針
- 04ミューテーションと楽観的更新
- 05DevTools とデバッグの指針
- 06機密状態の永続化に関する安全策
活用シーン
- 01API データの取得とキャッシュ
- 02クライアント状態ストアの構築
- 03ミューテーションと楽観的更新の実装
- 04サーバー状態と UI 状態の組み合わせ
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ asyrafhussin/ agent-skills --skill state-managementインストール後、Claude Code で/state-managementを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
AGENTS.mdmetadata.jsonREADME.mdrules/_sections.md_template.mdrq-cache-time.mdrq-dependent-queries.mdrq-enabled-option.mdrq-infinite-queries.mdrq-initial-data.mdrq-mutation-callbacks.mdrq-mutation-setup.mdrq-mutation-side-effects.mdrq-mutation-variables.mdrq-optimistic-updates.mdrq-paginated-queries.mdrq-parallel-queries.mdrq-placeholder-data.mdrq-prefetching.mdrq-query-cancellation.mdrq-query-functions.mdrq-query-invalidation.mdrq-query-keys.mdrq-refetch-options.mdrq-retry-logic.mdrq-select-transform.mdrq-stale-time.mdrq-suspense-mode.mdrq-usequery.mdzs-create-store.md詳細
React Query + Zustand による状態管理
バージョン 1.1.0 | TanStack Query v5 | Zustand v5 | 2026 年 3 月
注: このドキュメントでは、TanStack Query v5 と Zustand v5 を使用する AI エージェントと LLM 向けに、 包括的なパターンを紹介します。すべての例は v5 API で検証済みです。 自動リファクタリング、コード生成、状態管理のベストプラクティスに最適化されています。
v5 の破壊的変更(クイックリファレンス)
TanStack Query v5:
cacheTime→gcTimekeepPreviousDataオプション →placeholderData: keepPreviousData(インポートしたヘルパー)isPreviousData→isPlaceholderDatauseQueryからonSuccess/onError/onSettledを削除 —useMutationでは引き続き有効useQueryのsuspense: trueを削除 →useSuspenseQueryを使用
Zustand v5:
- 第 2 引数としての
shallowを削除 →zustand/shallowのuseShallowを使用 - 新しい参照を返すセレクターでは、無限ループを避けるために
useShallowが必要です
セキュリティ:Persist ミドルウェア
認証トークン、パスワード、シークレットを localStorage/sessionStorage に永続化しないでください。
partializeを使用して、機密性のない状態のみを永続化してください。トークンは HttpOnly Cookie で管理してください。
サーバー状態(React Query)とクライアント状態(Zustand)を扱う包括的なパターンです。効率的なデータ取得と状態管理のための 26 以上のルールを収録しています。
適用するタイミング
以下の場合に、これらのガイドラインを参照してください:
- API からデータを取得するとき
- サーバー状態とキャッシュを管理するとき
- ミューテーションと楽観的更新を扱うとき
- クライアント側ストアを作成するとき
- React Query と Zustand を組み合わせるとき
優先度別のルールカテゴリ
| 優先度 | カテゴリ | 影響度 | プレフィックス |
|---|---|---|---|
| 1 | React Query の基本 | 重大 | rq- |
| 2 | Zustand ストアのパターン | 重大 | zs- |
| 3 | キャッシュと無効化 | 高 | cache- |
| 4 | ミューテーションと更新 | 高 | mut- |
| 5 | 楽観的更新 | 中 | opt- |
| 6 | DevTools とデバッグ | 中 | dev- |
| 7 | 高度なパターン | 低 | adv- |
クイックリファレンス
1. React Query の基本(重大)
rq-setup- QueryClient と Provider のセットアップrq-usequery- 基本的な useQuery パターンrq-querykeys- Query キーの整理rq-loading-error- ローディング状態とエラー状態の処理rq-enabled- 条件付きクエリ
2. Zustand ストアのパターン(重大)
zs-create-store- 基本的なストアの作成zs-typescript- TypeScript のストアパターンzs-selectors- 効率的なセレクターzs-actions- アクションのパターンzs-persist- 状態をストレージに永続化
3. キャッシュと無効化(高)
cache-stale-time- stale time の設定cache-gc-time- ガベージコレクション時間の設定cache-invalidation- クエリの無効化cache-prefetch- データのプリフェッチcache-initial-data- 初期データの設定
4. ミューテーションと更新(高)
mut-usemutation- 基本的な useMutationmut-callbacks- onSuccess、onError コールバックmut-invalidate- ミューテーション後の無効化mut-update-cache- キャッシュの直接更新
5. 楽観的更新(中)
opt-basic- 基本的な楽観的更新opt-rollback- エラー時のロールバックopt-variables- ミューテーション変数の使用
6. DevTools とデバッグ(中)
dev-react-query- React Query DevToolsdev-zustand- Zustand DevToolsdev-debugging- デバッグ戦略
7. 高度なパターン(低)
adv-infinite-queries- 無限スクロールadv-parallel-queries- 並列リクエストadv-dependent-queries- 依存クエリadv-query-zustand- RQ と Zustand の組み合わせ
React Query のパターン
セットアップ
// lib/queryClient.ts
import { QueryClient } from '@tanstack/react-query'
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 30, // 30 minutes (formerly cacheTime)
retry: 1,
refetchOnWindowFocus: false,
},
},
})
// App.tsx
import { QueryClientProvider } from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
import { queryClient } from './lib/queryClient'
function App() {
return (
<QueryClientProvider client={queryClient}>
<Router />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
)
}
Query キーファクトリー
// lib/queryKeys.ts
export const queryKeys = {
// All posts
posts: {
all: ['posts'] as const,
lists: () => [...queryKeys.posts.all, 'list'] as const,
list: (filters: PostFilters) =>
[...queryKeys.posts.lists(), filters] as const,
details: () => [...queryKeys.posts.all, 'detail'] as const,
detail: (id: number) => [...queryKeys.posts.details(), id] as const,
},
// All users
users: {
all: ['users'] as const,
detail: (id: number) => [...queryKeys.users.all, id] as const,
posts: (userId: number) => [...queryKeys.users.all, userId, 'posts'] as const,
},
}
useQuery フック
// hooks/usePosts.ts
import { useQuery } from '@tanstack/react-query'
import { queryKeys } from '@/lib/queryKeys'
import { fetchPosts, fetchPost } from '@/api/posts'
export function usePosts(filters?: PostFilters) {
return useQuery({
queryKey: queryKeys.posts.list(filters ?? {}),
queryFn: () => fetchPosts(filters),
})
}
export function usePost(id: number) {
return useQuery({
queryKey: queryKeys.posts.detail(id),
queryFn: () => fetchPost(id),
enabled: !!id, // Only run if id exists
})
}
// Usage in component
function PostList() {
const { data: posts, isLoading, error } = usePosts()
if (isLoading) return <Spinner />
if (error) return <Error message={error.message} />
return (
<ul>
{posts?.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
useMutation フック
// hooks/useCreatePost.ts
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { queryKeys } from '@/lib/queryKeys'
import { createPost } from '@/api/posts'
export function useCreatePost() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: createPost,
onSuccess: (newPost) => {
// Invalidate and refetch posts list
queryClient.invalidateQueries({
queryKey: queryKeys.posts.lists(),
})
},
onError: (error) => {
console.error('Failed to create post:', error)
},
})
}
// Usage
function CreatePostForm() {
const { mutate, isPending } = useCreatePost()
const handleSubmit = (data: CreatePostData) => {
mutate(data)
}
return (
<form onSubmit={handleSubmit}>
{/* form fields */}
<button disabled={isPending}>
{isPending ? 'Creating...' : 'Create'}
</button>
</form>
)
}
楽観的更新
export function useUpdatePost() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: updatePost,
onMutate: async (updatedPost) => {
// Cancel outgoing refetches
await queryClient.cancelQueries({
queryKey: queryKeys.posts.detail(updatedPost.id),
})
// Snapshot previous value
const previousPost = queryClient.getQueryData(
queryKeys.posts.detail(updatedPost.id)
)
// Optimistically update
queryClient.setQueryData(
queryKeys.posts.detail(updatedPost.id),
updatedPost
)
return { previousPost }
},
onError: (err, updatedPost, context) => {
// Rollback on error
queryClient.setQueryData(
queryKeys.posts.detail(updatedPost.id),
context?.previousPost
)
},
onSettled: (data, error, variables) => {
// Refetch after settle
queryClient.invalidateQueries({
queryKey: queryKeys.posts.detail(variables.id),
})
},
})
}
Zustand のパターン
基本的なストア
// stores/useCounterStore.ts
import { create } from 'zustand'
interface CounterState {
count: number
increment: () => void
decrement: () => void
reset: () => void
}
export const useCounterStore = create<CounterState>((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 }),
}))
// Usage
function Counter() {
const { count, increment, decrement } = useCounterStore()
return (
<div>
<span>{count}</span>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
)
}
TypeScript とミドルウェアを使用したストア
// stores/useAuthStore.ts
import { create } from 'zustand'
import { persist, devtools } from 'zustand/middleware'
interface User {
id: number
name: string
email: string
}
interface AuthState {
user: User | null
isAuthenticated: boolean
login: (user: User) => void
logout: () => void
}
// ✅ Never persist tokens to localStorage — use HttpOnly cookies server-side
export const useAuthStore = create<AuthState>()(
devtools(
persist(
(set) => ({
user: null,
isAuthenticated: false,
login: (user) =>
set({
user,
isAuthenticated: true,
}),
logout: () =>
set({
user: null,
isAuthenticated: false,
}),
}),
{
name: 'auth-storage',
// Only persist display info and auth flag — tokens must NOT be included
partialize: (state) => ({
user: state.user,
isAuthenticated: state.isAuthenticated,
}),
}
)
)
)
パフォーマンス向上のためのセレクター
// Use selectors to prevent unnecessary re-renders
function UserName() {
// Only re-renders when user.name changes
const name = useAuthStore((state) => state.user?.name)
return <span>{name}</span>
}
// Multiple selectors
function UserInfo() {
const user = useAuthStore((state) => state.user)
const isAuthenticated = useAuthStore((state) => state.isAuthenticated)
if (!isAuthenticated) return <LoginButton />
return <span>{user?.name}</span>
}
React Query + Zustand の組み合わせ
// Server state: React Query (what comes from API)
const { data: posts } = usePosts()
// Client state: Zustand (UI state)
const { selectedPostId, selectPost } = useUIStore()
// Use together
const selectedPost = posts?.find((p) => p.id === selectedPostId)
使い方
詳しい説明とコード例については、各ルールファイルを参照してください:
rules/rq-usequery.md
rules/rq-query-keys.md
rules/rq-mutation-setup.md
rules/rq-optimistic-updates.md
rules/zs-create-store.md
rules/zs-persist.md
rules/rq-query-invalidation.md
rules/rq-prefetching.md
参考資料
React Query(TanStack Query)
- TanStack Query ドキュメント
- React Query の概要
- クエリガイド
- ミューテーションガイド
- クエリキーガイド
- 楽観的更新
- 無限クエリ
- ページネーションクエリ
- React Query DevTools
Zustand
ライセンス
このスキルは、教育および開発目的で現状のまま提供されています。React Query は TanStack による MIT ライセンス、Zustand は Poimandres(pmnd.rs)による MIT ライセンスです。