| name | react-query |
| description | [Applies to: **/*.{js,jsx,ts,tsx}] Definitive guidelines for using TanStack Query (formerly React Query) to manage server state efficiently, ensure type safety, and optimize performance in React applications. |
| source | cursor_mdc |
TanStack Query (react-query) Best Practices
This document outlines the definitive best practices for using TanStack Query in our React applications. Adhering to these guidelines ensures consistent, performant, and maintainable data fetching and state management.
1. Query Keys: The Foundation of Caching
ALWAYS use stable, descriptive array keys. These are fundamental for caching, refetching, and invalidation. For dynamic data, embed parameters directly into the array.
โ
GOOD: Stable Array Keys & Key Factories
const USERS_KEY = ['users'];
const userKeys = {
all: ['users'] as const,
lists: () => [...userKeys.all, 'list'] as const,
list: (filters: { status?: string; page?: number }) =>
[...userKeys.lists(), { filters }] as const,
details: () => [...userKeys.all, 'detail'] as const,
detail: (id: string) => [...userKeys.details(), id] as const,
};
โ BAD: Unstable or Generic Keys
useQuery('users', fetchUsers);
useQuery(['users', { id: userId }], fetchUserById);
2. Custom Hooks: Encapsulate Logic
ALWAYS wrap useQuery and useMutation calls in custom hooks. This centralizes data fetching logic, improves reusability, enhances type safety, and keeps components clean.
โ
GOOD: Dedicated Custom Hooks
import { useQuery } from '@tanstack/react-query';
import { fetchUsers, User } from '../api';
const userKeys = {
all: ['users'] as const,
list: (filters: { status?: string }) => [...userKeys.all, { filters }] as const,
};
export function useUsers(filters?: { status?: string }) {
return useQuery<User[], Error>({
queryKey: userKeys.list(filters || {}),
queryFn: () => fetchUsers(filters),
});
}
import { useUsers } from '../hooks/useUsers';
function UserList({ statusFilter }: { statusFilter?: string }) {
const { data: users, isLoading, error } = useUsers({ status: statusFilter });
if (isLoading) return Loading users...;
(error) ;
(
);
}
โ BAD: Direct useQuery in Components
import { useQuery } from '@tanstack/react-query';
import { fetchUsers } from '../api';
function UserList({ statusFilter }: { statusFilter?: string }) {
const { data: users, isLoading, error } = useQuery({
queryKey: ['users', { status: statusFilter }],
queryFn: () => fetchUsers({ status: statusFilter }),
});
}
3. Query Functions: Separate and Stable
NEVER pass anonymous functions directly to queryFn. ALWAYS declare queryFn separately to ensure stability, prevent unnecessary re-renders, and improve testability.
โ
GOOD: Separated Query Functions
export async function fetchUserById(id: string): Promise<User> {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) throw new Error('Failed to fetch user');
return response.json();
}
import { useQuery } from '@tanstack/react-query';
import { fetchUserById } from '../api';
const userKeys = {
detail: (id: string) => ['users', id] as const,
};
export function useUser(userId: string) {
return useQuery({
queryKey: userKeys.detail(userId),
queryFn: () => fetchUserById(userId),
: !!userId,
});
}
โ BAD: Anonymous Query Functions
import { useQuery } from '@tanstack/react-query';
export function useUser(userId: string) {
return useQuery({
queryKey: ['users', userId],
queryFn: async () => {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) throw new Error('Failed to fetch user');
return response.json();
},
enabled: !!userId,
});
}
4. Conditional Fetching: Use enabled
ALWAYS use the enabled option for conditional fetching. This is the explicit and recommended way to control when a query runs.
โ
GOOD: Using enabled
import { useQuery } from '@tanstack/react-query';
import { fetchUserProfile } from '../api';
export function useUserProfile(userId?: string) {
return useQuery({
queryKey: ['userProfile', userId],
queryFn: () => fetchUserProfile(userId!),
enabled: !!userId,
});
}
โ BAD: Conditional Hook Calls
import { useUserProfile } from '../hooks/useUserProfile';
function UserProfile({ userId }: { userId?: string }) {
if (!userId) {
return null;
}
const { data: user, isLoading } = useUserProfile(userId);
}
5. Data Transformation: Use select
ALWAYS use the select option within useQuery for transforming or filtering data. This ensures the transformation happens once at the query level, optimizing performance and preventing redundant calculations in components.
โ
GOOD: select for Transformations
import { useQuery } from '@tanstack/react-query';
import { fetchUsers, User } from '../api';
export function useActiveUsers() {
return useQuery<User[], Error, string[]>({
queryKey: ['users', 'all'],
queryFn: fetchUsers,
select: (data) => data.filter(user => user.status === 'active').map(user => user.name),
});
}
import { useActiveUsers } from '../hooks/useActiveUsers';
function ActiveUserNames() {
const { data: activeUserNames, isLoading } = useActiveUsers();
if (isLoading) return <div>Loading active users...</div>;
return (
<>
{activeUserNames?.map((name) => (
{name}
))}
);
}
โ BAD: Transforming Data in Every Component
import { useQuery } from '@tanstack/react-query';
import { fetchUsers } from '../api';
function ActiveUserNames() {
const { data: users, isLoading } = useQuery({
queryKey: ['users', 'all'],
queryFn: fetchUsers,
});
const activeUserNames = users?.filter(user => user.status === 'active').map(user => user.name);
if (isLoading) return <div>Loading active users...</div>;
return (
<ul>
{activeUserNames?.map((name) => (
<li key={name}>{name}</li>
))}
</ul>
);
}
6. Mutations and Cache Invalidation
ALWAYS use useMutation for CUD (Create, Update, Delete) operations. After a successful mutation, ALWAYS invalidate relevant queries to ensure the UI reflects the latest server state. For immediate feedback, consider optimistic updates with setQueryData.
โ
GOOD: Invalidation after Mutation
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { createTodo, Todo } from '../api';
export function useCreateTodo() {
const queryClient = useQueryClient();
return useMutation<Todo, Error, { title: string }>({
mutationFn: createTodo,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
});
}
import { useCreateTodo } from '../hooks/useCreateTodo';
function TodoForm() {
const { mutate, isLoading } = useCreateTodo();
const handleSubmit = (event: React.FormEvent) => {
event.preventDefault();
const formData = new FormData(event.currentTarget as HTMLFormElement);
const title = formData.() ;
({ title });
};
(
);
}
โ
GOOD: Optimistic Updates
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { updateTodo, Todo } from '../api';
export function useUpdateTodo() {
const queryClient = useQueryClient();
return useMutation<Todo, Error, Partial<Todo> & { id: string }>({
mutationFn: updateTodo,
onMutate: async (newTodo) => {
await queryClient.cancelQueries({ queryKey: ['todos'] });
const previousTodos = queryClient.getQueryData<Todo[]>(['todos']);
queryClient.setQueryData<Todo[]>(['todos'], (old) =>
old ? old.map((todo) => (todo.id === newTodo.id ? { ...todo, ...newTodo } : todo)) : []
);
return { previousTodos };
},
onError: (err, newTodo, context) => {
queryClient.([], context?.);
},
: {
queryClient.({ : [] });
},
});
}
7. Performance: Prefetching & Defaults
LEVERAGE TanStack Query's defaults (e.g., staleTime: 0, automatic retries, refetch on window focus) and STRATEGICALLY use prefetching for critical user flows.
โ
GOOD: Prefetching for Router Integration
import { QueryClient } from '@tanstack/react-query';
import { fetchProjectById } from '../api';
export const projectLoader = (queryClient: QueryClient) => async ({ params }: { params: { projectId: string } }) => {
const queryKey = ['projects', params.projectId];
await queryClient.prefetchQuery({
queryKey,
queryFn: () => fetchProjectById(params.projectId),
});
return null;
};
import { Link } from 'react-router-dom';
import { useQueryClient } from '@tanstack/react-query';
import { fetchProjectById } from '../api';
function ProjectLink({ projectId, projectName }: { projectId: string; projectName: string }) {
queryClient = ();
= () => {
queryClient.({
: [, projectId],
: (projectId),
: * * ,
});
};
(
);
}
8. ESLint Plugin: Enforce Standards
ALWAYS install and configure the @tanstack/query-eslint-plugin. It enforces many of these best practices automatically, catching common mistakes early.
{
"plugins": ["@tanstack/query"],
"rules": {
"@tanstack/query/exhaustive-deps": "error",
"@tanstack/query/prefer-query-object": "error",
"@tanstack/query/stable-query-client": "error"
}
}