TanStack React Query patterns - use for data fetching, caching, mutations, optimistic updates, and server state management
Implements TanStack React Query patterns for server state management. Use this when building data fetching hooks with caching, mutations, optimistic updates, and prefetching for React applications.
/plugin marketplace add ashchupliak/dream-team/plugin install dream-team@dream-team-marketplaceThis skill inherits all available tools. When active, it can use any tool Claude has access to.
// providers/QueryProvider.tsx
'use client'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
import { useState } from 'react'
export function QueryProvider({ children }: { children: React.ReactNode }) {
const [queryClient] = useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000, // 1 minute
gcTime: 5 * 60 * 1000, // 5 minutes (formerly cacheTime)
refetchOnWindowFocus: false,
retry: 1,
},
},
})
)
return (
<QueryClientProvider client={queryClient}>
{children}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
)
}
// lib/queryKeys.ts
export const queryKeys = {
environments: {
all: ['environments'] as const,
lists: () => [...queryKeys.environments.all, 'list'] as const,
list: (filters: EnvironmentFilters) =>
[...queryKeys.environments.lists(), filters] as const,
details: () => [...queryKeys.environments.all, 'detail'] as const,
detail: (id: string) => [...queryKeys.environments.details(), id] as const,
},
users: {
all: ['users'] as const,
detail: (id: string) => [...queryKeys.users.all, id] as const,
},
}
// hooks/useEnvironments.ts
import { useQuery } from '@tanstack/react-query'
import { queryKeys } from '@/lib/queryKeys'
interface EnvironmentFilters {
status?: string
page?: number
}
async function fetchEnvironments(filters: EnvironmentFilters) {
const params = new URLSearchParams()
if (filters.status) params.set('status', filters.status)
if (filters.page) params.set('page', String(filters.page))
const res = await fetch(`/api/environments?${params}`)
if (!res.ok) throw new Error('Failed to fetch environments')
return res.json()
}
export function useEnvironments(filters: EnvironmentFilters = {}) {
return useQuery({
queryKey: queryKeys.environments.list(filters),
queryFn: () => fetchEnvironments(filters),
})
}
// Usage
function EnvironmentList() {
const { data, isLoading, error } = useEnvironments({ status: 'RUNNING' })
if (isLoading) return <Skeleton />
if (error) return <Error message={error.message} />
return (
<ul>
{data?.map((env) => (
<li key={env.id}>{env.name}</li>
))}
</ul>
)
}
// hooks/useEnvironment.ts
export function useEnvironment(id: string) {
return useQuery({
queryKey: queryKeys.environments.detail(id),
queryFn: async () => {
const res = await fetch(`/api/environments/${id}`)
if (!res.ok) {
if (res.status === 404) return null
throw new Error('Failed to fetch environment')
}
return res.json()
},
enabled: !!id, // Don't fetch if no id
})
}
// hooks/useCreateEnvironment.ts
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { queryKeys } from '@/lib/queryKeys'
interface CreateEnvironmentInput {
name: string
description?: string
}
export function useCreateEnvironment() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (input: CreateEnvironmentInput) => {
const res = await fetch('/api/environments', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(input),
})
if (!res.ok) {
const error = await res.json()
throw new Error(error.message || 'Failed to create environment')
}
return res.json()
},
onSuccess: () => {
// Invalidate and refetch
queryClient.invalidateQueries({
queryKey: queryKeys.environments.lists(),
})
},
})
}
// Usage
function CreateEnvironmentForm() {
const mutation = useCreateEnvironment()
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
const formData = new FormData(e.currentTarget)
mutation.mutate({
name: formData.get('name') as string,
description: formData.get('description') as string,
})
}
return (
<form onSubmit={handleSubmit}>
<input name="name" required />
<textarea name="description" />
<button type="submit" disabled={mutation.isPending}>
{mutation.isPending ? 'Creating...' : 'Create'}
</button>
{mutation.isError && (
<p className="text-red-500">{mutation.error.message}</p>
)}
</form>
)
}
// hooks/useUpdateEnvironment.ts
export function useUpdateEnvironment() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async ({ id, ...data }: UpdateEnvironmentInput) => {
const res = await fetch(`/api/environments/${id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
})
if (!res.ok) throw new Error('Failed to update')
return res.json()
},
// Optimistic update
onMutate: async (newData) => {
// Cancel outgoing refetches
await queryClient.cancelQueries({
queryKey: queryKeys.environments.detail(newData.id),
})
// Snapshot previous value
const previousEnv = queryClient.getQueryData(
queryKeys.environments.detail(newData.id)
)
// Optimistically update
queryClient.setQueryData(
queryKeys.environments.detail(newData.id),
(old: Environment) => ({ ...old, ...newData })
)
return { previousEnv }
},
// Rollback on error
onError: (err, newData, context) => {
if (context?.previousEnv) {
queryClient.setQueryData(
queryKeys.environments.detail(newData.id),
context.previousEnv
)
}
},
// Refetch after success or error
onSettled: (data, error, variables) => {
queryClient.invalidateQueries({
queryKey: queryKeys.environments.detail(variables.id),
})
},
})
}
// hooks/useDeleteEnvironment.ts
export function useDeleteEnvironment() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (id: string) => {
const res = await fetch(`/api/environments/${id}`, {
method: 'DELETE',
})
if (!res.ok) throw new Error('Failed to delete')
},
onMutate: async (deletedId) => {
await queryClient.cancelQueries({
queryKey: queryKeys.environments.lists(),
})
const previousEnvs = queryClient.getQueryData<Environment[]>(
queryKeys.environments.lists()
)
// Remove from list optimistically
queryClient.setQueryData<Environment[]>(
queryKeys.environments.lists(),
(old) => old?.filter((env) => env.id !== deletedId)
)
return { previousEnvs }
},
onError: (err, id, context) => {
queryClient.setQueryData(
queryKeys.environments.lists(),
context?.previousEnvs
)
},
onSettled: () => {
queryClient.invalidateQueries({
queryKey: queryKeys.environments.lists(),
})
},
})
}
// hooks/useInfiniteEnvironments.ts
import { useInfiniteQuery } from '@tanstack/react-query'
export function useInfiniteEnvironments() {
return useInfiniteQuery({
queryKey: ['environments', 'infinite'],
queryFn: async ({ pageParam }) => {
const res = await fetch(`/api/environments?cursor=${pageParam}`)
return res.json()
},
initialPageParam: '',
getNextPageParam: (lastPage) => lastPage.nextCursor,
})
}
// Usage
function InfiniteList() {
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =
useInfiniteEnvironments()
return (
<>
{data?.pages.map((page, i) => (
<Fragment key={i}>
{page.items.map((env) => (
<EnvironmentCard key={env.id} environment={env} />
))}
</Fragment>
))}
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
>
{isFetchingNextPage ? 'Loading...' : hasNextPage ? 'Load More' : 'No more'}
</button>
</>
)
}
// Prefetch on hover
function EnvironmentLink({ id, name }: { id: string; name: string }) {
const queryClient = useQueryClient()
const prefetch = () => {
queryClient.prefetchQuery({
queryKey: queryKeys.environments.detail(id),
queryFn: () => fetchEnvironment(id),
staleTime: 60 * 1000,
})
}
return (
<Link href={`/environments/${id}`} onMouseEnter={prefetch}>
{name}
</Link>
)
}
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.