react-query — Claude Code skill: React Query rules for your agent
Tells the agent how to fetch and cache server data in React: queries, mutations, invalidation and optimistic updates.
npx skills add Mindrally/skills --skill react-query -a claude-codeWhat react-query does
The skill teaches the agent how React Query (now TanStack Query) works and keeps it writing code that way. Server data goes through useQuery and useMutation instead of useState. Client state stays with React Context and useReducer, or with Zustand — the skill shows both.
Inside you get a project layout with queries.ts and mutations.ts next to each feature, a QueryClientProvider setup with staleTime, cacheTime and retry, plus patterns for dependent queries, pagination and infinite scroll via useInfiniteQuery. Optimistic updates have their own section: cancel the query, keep the previous value, roll back in onError. Query keys are meant to live in one queryKeys object with factories rather than inline strings.
The examples import from react-query, not @tanstack/react-query. On TanStack Query v5 they break: cacheTime is gone and keepPreviousData became placeholderData. Check which version your project runs before letting the agent copy anything.
What it helps with
- separate server state from client state in a React app
- set up caching and background refetching without hand-written useEffect
- do an optimistic update that rolls back on failure
- build pagination and infinite scroll with useInfiniteQuery
- keep query keys consistent through a key factory
Example prompts for the agent
- set up React Query in this project and move query hooks into their own files
- add an optimistic update for editing the user profile
- rewrite the user list loading with useInfiniteQuery
How to install react-query
1Skills CLI (Vercel)
npx skills add Mindrally/skills --skill react-query -a claude-code2Manually: put the files in place
~/.claude/skills/react-query/SKILL.mdCopy the whole skill folder; for one project use .claude/skills/react-query/
1Skills CLI (Vercel)
npx skills add Mindrally/skills --skill react-query -a codex2Manually: put the files in place
.agents/skills/react-query/SKILL.mdFor all projects use ~/.agents/skills/
1Skills CLI (Vercel)
npx skills add Mindrally/skills --skill react-query -a cursor2Manually: put the files in place
.cursor/skills/react-query/SKILL.mdCursor also reads .agents/skills/ and .claude/skills/
1GitHub CLI
gh skill install Mindrally/skills react-query2Manually: put the files in place
.github/skills/react-query/SKILL.md1Skills CLI (Vercel)
npx skills add Mindrally/skills --skill react-query -a gemini-cli2Platform CLI
gemini skills install https://github.com/Mindrally/skills --path react-queryManually: put the files in place
.devin/skills/react-query/SKILL.mdThe legacy .windsurf/skills/ path still works
Skills CLI (Vercel)
npx skills add Mindrally/skills --skill react-query -a opencodeClaude.ai
Zip the folder and upload it in Settings → Capabilities → Skills
Step by step, with commands for each platform: How to install skills in Claude Code: five ways
Questions about react-query
How do I install react-query in Claude?
Run npx skills add Mindrally/skills --skill react-query -a claude-code. The skill lands in ~/.claude/skills/react-query/SKILL.md.
What does the react-query skill do?
It gives the agent rules for React Query: queries, mutations, caching, invalidation and optimistic updates. The agent follows them whenever it writes data-fetching code in React.
Which React Query versions does the skill cover?
The examples use the react-query package with the older API: cacheTime, keepPreviousData. For TanStack Query v5 they need rewriting.
Source text
SKILL.md · Apache-2.0
---
name: react-query
description: Guidelines for using React Query for data fetching, caching, and server state synchronization in React applications
metadata:
maintainer: Mindrally
source: https://github.com/Mindrally/skills
---
# React Query Best Practices
You are an expert in React Query, TypeScript, and React development. React Query (now TanStack Query) simplifies data fetching logic with built-in caching, background updates, and stale data management.
## Core Principles
- Use React Query for all data fetching and caching
- Leverage React Query's built-in state management instead of `useState` for server data
- Use React Context and `useReducer` for managing client-side global state
- Avoid excessive API calls through proper caching strategies
- Always handle loading states and errors properly
## Project Structure
```
src/
components/
[Feature]/
index.tsx
queries.ts # Feature-specific query hooks
mutations.ts # Feature-specific mutation hooks
hooks/
useAuth.ts
useApi.ts
services/
api/
client.ts # Axios/fetch configuration
users.ts # User API functions
posts.ts # Post API functions
providers/
ReactQueryProvider.tsx
types/
index.ts
```
## Setup
### Provider Configuration
```typescript
// providers/ReactQueryProvider.tsx
import { QueryClient, QueryClientProvider } from 'react-query';
import { ReactQueryDevtools } from 'react-query/devtools';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // 5 minutes
cacheTime: 30 * 60 * 1000, // 30 minutes
retry: 2,
refetchOnWindowFocus: true,
},
},
});
export function ReactQueryProvider({ children }: { children: React.ReactNode }) {
return (
<QueryClientProvider client={queryClient}>
{children}
<ReactQueryDevtools />
</QueryClientProvider>
);
}
```
## Query Patterns
### Basic Query Hook
```typescript
import { useQuery } from 'react-query';
import { fetchUser, User } from '@/services/api/users';
export function useUser(userId: string) {
return useQuery<User, Error>(
['user', userId],
() => fetchUser(userId),
{
enabled: !!userId,
staleTime: 1000 * 60 * 10, // 10 minutes
}
);
}
```
### Query with Error Handling
Services should throw user-friendly errors that React Query can catch and display:
```typescript
// services/api/users.ts
export async function fetchUser(userId: string): Promise<User> {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
// Throw user-friendly error message
throw new Error('Unable to load user profile. Please try again.');
}
return response.json();
}
// Component usage
function UserProfile({ userId }: { userId: string }) {
const { data: user, isLoading, error } = useUser(userId);
if (isLoading) return <LoadingSpinner />;
if (error) return <ErrorMessage message={error.message} />;
return <ProfileCard user={user} />;
}
```
### Dependent Queries
```typescript
function useUserWithPosts(userId: string) {
const userQuery = useUser(userId);
const postsQuery = useQuery(
['posts', userId],
() => fetchUserPosts(userId),
{
enabled: !!userQuery.data,
}
);
return { userQuery, postsQuery };
}
```
### Paginated Queries
```typescript
function usePaginatedUsers(page: number, limit: number = 10) {
return useQuery(
['users', 'list', { page, limit }],
() => fetchUsers({ page, limit }),
{
keepPreviousData: true,
}
);
}
```
### Infinite Scroll
```typescript
import { useInfiniteQuery } from 'react-query';
function useInfiniteUsers() {
return useInfiniteQuery(
['users', 'infinite'],
({ pageParam = 1 }) => fetchUsers({ page: pageParam }),
{
getNextPageParam: (lastPage) => lastPage.nextPage ?? undefined,
}
);
}
```
## Mutation Patterns
### Basic Mutation
```typescript
import { useMutation, useQueryClient } from 'react-query';
function useCreateUser() {
const queryClient = useQueryClient();
return useMutation(createUser, {
onSuccess: () => {
queryClient.invalidateQueries(['users']);
},
onError: (error: Error) => {
toast.error(error.message);
},
});
}
```
### Optimistic Updates
```typescript
function useUpdateUser() {
const queryClient = useQueryClient();
return useMutation(updateUser, {
onMutate: async (updatedUser) => {
await queryClient.cancelQueries(['user', updatedUser.id]);
const previousUser = queryClient.getQueryData(['user', updatedUser.id]);
queryClient.setQueryData(['user', updatedUser.id], updatedUser);
return { previousUser };
},
onError: (err, updatedUser, context) => {
if (context?.previousUser) {
queryClient.setQueryData(['user', updatedUser.id], context.previousUser);
}
},
onSettled: (data, error, updatedUser) => {
queryClient.invalidateQueries(['user', updatedUser.id]);
},
});
}
```
## State Management Integration
### Combining with Context/Reducer
Use React Query for server state and Context/Reducer for client state:
```typescript
// Client state with Context
const AppStateContext = createContext<AppState | undefined>(undefined);
const AppDispatchContext = createContext<Dispatch<Action> | undefined>(undefined);
function AppProvider({ children }: { children: React.ReactNode }) {
const [state, dispatch] = useReducer(appReducer, initialState);
return (
<AppStateContext.Provider value={state}>
<AppDispatchContext.Provider value={dispatch}>
{children}
</AppDispatchContext.Provider>
</AppStateContext.Provider>
);
}
// Server state with React Query
function UserDashboard() {
const { theme } = useAppState(); // Client state
const { data: user } = useUser(userId); // Server state
return <Dashboard theme={theme} user={user} />;
}
```
### Combining with Zustand (Alternative)
```typescript
import { create } from 'zustand';
// Client state store
const useStore = create((set) => ({
theme: 'light',
setTheme: (theme) => set({ theme }),
}));
// Component using both
function App() {
const theme = useStore((state) => state.theme);
const { data: user } = useUser(userId);
return <Layout theme={theme} user={user} />;
}
```
## Performance Optimization
### Query Key Best Practices
```typescript
// Structured query keys
const queryKeys = {
users: {
all: ['users'] as const,
lists: () => [...queryKeys.users.all, 'list'] as const,
list: (filters: Filters) => [...queryKeys.users.lists(), filters] as const,
details: () => [...queryKeys.users.all, 'detail'] as const,
detail: (id: string) => [...queryKeys.users.details(), id] as const,
},
};
```
### Selective Subscriptions
```typescript
// Only subscribe to user name changes
function useUserName(userId: string) {
return useUser(userId, {
select: (user) => user.name,
});
}
```
### Prefetching
```typescript
function UserListItem({ userId }: { userId: string }) {
const queryClient = useQueryClient();
const handleMouseEnter = () => {
queryClient.prefetchQuery(
['user', userId],
() => fetchUser(userId),
{ staleTime: 60000 }
);
};
return (
<li onMouseEnter={handleMouseEnter}>
<Link to={`/users/${userId}`}>View Profile</Link>
</li>
);
}
```
## Error Handling Patterns
### Global Error Handler
```typescript
const queryClient = new QueryClient({
defaultOptions: {
queries: {
onError: (error: Error) => {
console.error('Query error:', error);
},
},
mutations: {
onError: (error: Error) => {
toast.error(error.message);
},
},
},
});
```
### Error Boundaries
```typescript
import { QueryErrorResetBoundary } from 'react-query';
import { ErrorBoundary } from 'react-error-boundary';
function App() {
return (
<QueryErrorResetBoundary>
{({ reset }) => (
<ErrorBoundary
onReset={reset}
fallbackRender={({ error, resetErrorBoundary }) => (
<div>
<p>Something went wrong: {error.message}</p>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
)}
>
<UserProfile />
</ErrorBoundary>
)}
</QueryErrorResetBoundary>
);
}
```
## Key Conventions
1. Use React Query DevTools to inspect cache and track query status
2. Group react-query hooks within feature-specific directories (feature-based organization)
3. Always handle errors properly with user-friendly messages and retry options
4. Fetch only required data - use API parameters to reduce data transfer
5. Avoid deeply nesting queries - flatten when possible for better performance
6. Use local state for component-specific data, global state for shared data
7. Leverage React Query's built-in caching and state management capabilities
## Anti-Patterns to Avoid
- Do not use `useEffect` for data fetching
- Do not store server data in `useState`
- Do not forget loading and error state handling
- Do not create queries without proper cache invalidation strategies
- Do not skip the `enabled` option for conditional queries
- Do not ignore TypeScript types for query responses
© Mindrally · Apache-2.0 · Mindrally/skills