--- name: state-management description: React Query and Zustand patterns for state management. Use when implementing data fetching, caching, mutations, or client-side state. Triggers on tasks involving useQuery, useMutation, Zustand stores, caching, or state management. version: 1.0.0 date: January 2026 --- # State Management with React Query + Zustand **Version 1.0.0** State Management Patterns January 2026 > **Note:** > This document provides comprehensive patterns for AI agents and LLMs working with > React Query (TanStack Query) and Zustand. Optimized for automated refactoring, > code generation, and state management best practices. --- Comprehensive patterns for server state (React Query) and client state (Zustand). Contains 26+ rules for efficient data fetching and state management. ## When to Apply Reference these guidelines when: - Fetching data from APIs - Managing server state and caching - Handling mutations and optimistic updates - Creating client-side stores - Combining React Query with Zustand ## Rule Categories by Priority | Priority | Category | Impact | Prefix | |----------|----------|--------|--------| | 1 | React Query Basics | CRITICAL | `rq-` | | 2 | Zustand Store Patterns | CRITICAL | `zs-` | | 3 | Caching & Invalidation | HIGH | `cache-` | | 4 | Mutations & Updates | HIGH | `mut-` | | 5 | Optimistic Updates | MEDIUM | `opt-` | | 6 | DevTools & Debugging | MEDIUM | `dev-` | | 7 | Advanced Patterns | LOW | `adv-` | ## Quick Reference ### 1. React Query Basics (CRITICAL) - `rq-setup` - QueryClient and Provider setup - `rq-usequery` - Basic useQuery patterns - `rq-querykeys` - Query key organization - `rq-loading-error` - Handle loading and error states - `rq-enabled` - Conditional queries ### 2. Zustand Store Patterns (CRITICAL) - `zs-create-store` - Create basic store - `zs-typescript` - TypeScript store patterns - `zs-selectors` - Efficient selectors - `zs-actions` - Action patterns - `zs-persist` - Persist state to storage ### 3. Caching & Invalidation (HIGH) - `cache-stale-time` - Configure stale time - `cache-gc-time` - Configure garbage collection - `cache-invalidation` - Invalidate queries - `cache-prefetch` - Prefetch data - `cache-initial-data` - Set initial data ### 4. Mutations & Updates (HIGH) - `mut-usemutation` - Basic useMutation - `mut-callbacks` - onSuccess, onError callbacks - `mut-invalidate` - Invalidate after mutation - `mut-update-cache` - Direct cache updates ### 5. Optimistic Updates (MEDIUM) - `opt-basic` - Basic optimistic updates - `opt-rollback` - Rollback on error - `opt-variables` - Use mutation variables ### 6. DevTools & Debugging (MEDIUM) - `dev-react-query` - React Query DevTools - `dev-zustand` - Zustand DevTools - `dev-debugging` - Debug strategies ### 7. Advanced Patterns (LOW) - `adv-infinite-queries` - Infinite scrolling - `adv-parallel-queries` - Parallel requests - `adv-dependent-queries` - Dependent queries - `adv-query-zustand` - Combine RQ with Zustand ## React Query Patterns ### Setup ```tsx // 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 ( ) } ``` ### Query Keys Factory ```tsx // 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 Hook ```tsx // 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 if (error) return return ( ) } ``` ### useMutation Hook ```tsx // 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 fields */}
) } ``` ### Optimistic Updates ```tsx 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 Patterns ### Basic Store ```tsx // stores/useCounterStore.ts import { create } from 'zustand' interface CounterState { count: number increment: () => void decrement: () => void reset: () => void } export const useCounterStore = create((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 (
{count}
) } ``` ### Store with TypeScript and Middleware ```tsx // 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 token: string | null isAuthenticated: boolean login: (user: User, token: string) => void logout: () => void } export const useAuthStore = create()( devtools( persist( (set) => ({ user: null, token: null, isAuthenticated: false, login: (user, token) => set({ user, token, isAuthenticated: true, }), logout: () => set({ user: null, token: null, isAuthenticated: false, }), }), { name: 'auth-storage', partialize: (state) => ({ user: state.user, token: state.token, isAuthenticated: state.isAuthenticated, }), } ) ) ) ``` ### Selectors for Performance ```tsx // Use selectors to prevent unnecessary re-renders function UserName() { // Only re-renders when user.name changes const name = useAuthStore((state) => state.user?.name) return {name} } // Multiple selectors function UserInfo() { const user = useAuthStore((state) => state.user) const isAuthenticated = useAuthStore((state) => state.isAuthenticated) if (!isAuthenticated) return return {user?.name} } ``` ### Combining React Query + Zustand ```tsx // 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) ``` ## How to Use Read individual rule files for detailed explanations and code examples: ``` rules/rq-usequery.md rules/zs-create-store.md rules/cache-invalidation.md ``` --- ## References ### React Query (TanStack Query) 1. [TanStack Query Documentation](https://tanstack.com/query/latest) 2. [React Query Overview](https://tanstack.com/query/latest/docs/react/overview) 3. [Queries Guide](https://tanstack.com/query/latest/docs/react/guides/queries) 4. [Mutations Guide](https://tanstack.com/query/latest/docs/react/guides/mutations) 5. [Query Keys Guide](https://tanstack.com/query/latest/docs/react/guides/query-keys) 6. [Optimistic Updates](https://tanstack.com/query/latest/docs/react/guides/optimistic-updates) 7. [Infinite Queries](https://tanstack.com/query/latest/docs/react/guides/infinite-queries) 8. [Paginated Queries](https://tanstack.com/query/latest/docs/react/guides/paginated-queries) 9. [React Query DevTools](https://tanstack.com/query/latest/docs/react/devtools) ### Zustand 1. [Zustand Demo](https://zustand-demo.pmnd.rs) 2. [Zustand GitHub](https://github.com/pmndrs/zustand) 3. [Getting Started](https://docs.pmnd.rs/zustand/getting-started/introduction) 4. [TypeScript Guide](https://docs.pmnd.rs/zustand/guides/typescript) 5. [Persisting Store Data](https://docs.pmnd.rs/zustand/integrations/persisting-store-data) 6. [Zustand Recipes](https://github.com/pmndrs/zustand#recipes) --- ## License This skill is provided as-is for educational and development purposes. React Query is MIT licensed by TanStack. Zustand is MIT licensed by Poimandres (pmnd.rs).