mcpField
S
state-management

state-management

github.com/asyrafhussin開発95801 回インストール9 週間前に更新
リポジトリ

state-management について

TanStack Query v5 と Zustand v5 を使う React 開発者やコーディングエージェント向けのリファレンスです。サーバー・クライアント状態、キャッシュ、更新処理、安全な永続化を扱います。

主な機能

  1. 01TanStack Query v5 のパターン
  2. 02Zustand v5 のストアパターン
  3. 03キャッシュと無効化の指針
  4. 04ミューテーションと楽観的更新
  5. 05DevTools とデバッグの指針
  6. 06機密状態の永続化に関する安全策

活用シーン

  1. 01API データの取得とキャッシュ
  2. 02クライアント状態ストアの構築
  3. 03ミューテーションと楽観的更新の実装
  4. 04サーバー状態と UI 状態の組み合わせ

インストール

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

$npx skills add https://github.com/asyrafhussin/agent-skills --skill state-management

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

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

AGENTS.md
metadata.json
README.md
rules/
_sections.md
_template.md
rq-cache-time.md
rq-dependent-queries.md
rq-enabled-option.md
rq-infinite-queries.md
rq-initial-data.md
rq-mutation-callbacks.md
rq-mutation-setup.md
rq-mutation-side-effects.md
rq-mutation-variables.md
rq-optimistic-updates.md
rq-paginated-queries.md
rq-parallel-queries.md
rq-placeholder-data.md
rq-prefetching.md
rq-query-cancellation.md
rq-query-functions.md
rq-query-invalidation.md
rq-query-keys.md
rq-refetch-options.md
rq-retry-logic.md
rq-select-transform.md
rq-stale-time.md
rq-suspense-mode.md
rq-usequery.md
zs-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 → gcTime
  • keepPreviousData オプション → placeholderData: keepPreviousData(インポートしたヘルパー)
  • isPreviousData → isPlaceholderData
  • useQuery から 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 を組み合わせるとき

優先度別のルールカテゴリ

優先度カテゴリ影響度プレフィックス
1React Query の基本重大rq-
2Zustand ストアのパターン重大zs-
3キャッシュと無効化高cache-
4ミューテーションと更新高mut-
5楽観的更新中opt-
6DevTools とデバッグ中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 - 基本的な useMutation
  • mut-callbacks - onSuccess、onError コールバック
  • mut-invalidate - ミューテーション後の無効化
  • mut-update-cache - キャッシュの直接更新
5. 楽観的更新(中)
  • opt-basic - 基本的な楽観的更新
  • opt-rollback - エラー時のロールバック
  • opt-variables - ミューテーション変数の使用
6. DevTools とデバッグ(中)
  • dev-react-query - React Query DevTools
  • dev-zustand - Zustand DevTools
  • dev-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)
  1. TanStack Query ドキュメント
  2. React Query の概要
  3. クエリガイド
  4. ミューテーションガイド
  5. クエリキーガイド
  6. 楽観的更新
  7. 無限クエリ
  8. ページネーションクエリ
  9. React Query DevTools
Zustand
  1. Zustand デモ
  2. Zustand GitHub
  3. はじめに
  4. TypeScript ガイド
  5. ストアデータの永続化
  6. Zustand レシピ

ライセンス

このスキルは、教育および開発目的で現状のまま提供されています。React Query は TanStack による MIT ライセンス、Zustand は Poimandres(pmnd.rs)による MIT ライセンスです。

コメント