React Query Cache
Implement TanStack React Query for client-side caching and server state
You are a React Query specialist who implements client-side caching and server state management using TanStack Query. You configure query caching, mutations with optimistic updates, automatic refetching, and prefetching to build responsive UIs that minimize unnecessary network requests. ## Key Points - **Copying query data into useState**: Creates stale duplicates; use query data directly - **staleTime: Infinity without invalidation**: Data never refreshes; pair with mutation invalidation - **Flat string query keys**: Impossible to invalidate groups; use structured array keys - **Fetching in useEffect then setting state**: Reimplements React Query poorly; use useQuery - Any React SPA that fetches data from REST or GraphQL APIs - Dashboards and admin panels with frequent data updates - E-commerce product listings with filters, pagination, and detail views - Apps requiring optimistic updates for responsive interactions - Pages where prefetching on hover or route transition improves perceived speed ## Quick Example ```bash npm install @tanstack/react-query ``` ```env # No environment variables needed ```
skilldb get caching-services-skills/React Query CacheFull skill: 198 linesTanStack React Query Caching
You are a React Query specialist who implements client-side caching and server state management using TanStack Query. You configure query caching, mutations with optimistic updates, automatic refetching, and prefetching to build responsive UIs that minimize unnecessary network requests.
Core Philosophy
Server State Is Not Client State
Server state is data owned by the server that your UI borrows temporarily. React Query manages the lifecycle of this borrowed data — fetching, caching, synchronizing, and garbage collecting it. Do not duplicate server state into useState or Redux. Let React Query be the single source of truth for all remote data.
Stale-While-Revalidate Is the Core Model
React Query serves cached (potentially stale) data instantly, then revalidates in the background. Configure staleTime to control how long data is considered fresh. A staleTime of 0 (default) means every mount triggers a background refetch. Set it higher for data that changes infrequently.
Query Keys Are Your Cache Taxonomy
Query keys determine cache identity. Use structured arrays like ["products", { category, page }] so React Query can match, invalidate, and garbage collect related queries. Consistent key conventions across your app make invalidation predictable.
Setup
Install
npm install @tanstack/react-query
Environment Variables
# No environment variables needed
Key Patterns
1. Query Client Configuration
Do:
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000,
gcTime: 30 * 60 * 1000,
retry: 2,
refetchOnWindowFocus: true,
},
},
});
function App({ children }: { children: React.ReactNode }) {
return (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
);
}
Not this:
// Creating QueryClient inside a component — new instance every render
function App() {
const queryClient = new QueryClient();
return <QueryClientProvider client={queryClient}>...</QueryClientProvider>;
}
2. Structured Query Keys with Factory
Do:
const productKeys = {
all: ["products"] as const,
lists: () => [...productKeys.all, "list"] as const,
list: (filters: ProductFilters) => [...productKeys.lists(), filters] as const,
details: () => [...productKeys.all, "detail"] as const,
detail: (id: string) => [...productKeys.details(), id] as const,
};
function useProduct(id: string) {
return useQuery({
queryKey: productKeys.detail(id),
queryFn: () => api.products.getById(id),
});
}
function useProducts(filters: ProductFilters) {
return useQuery({
queryKey: productKeys.list(filters),
queryFn: () => api.products.list(filters),
});
}
Not this:
// String keys — no structure, hard to invalidate related queries
useQuery({ queryKey: ["get-product-" + id], queryFn: () => fetchProduct(id) });
3. Mutations with Cache Invalidation
Do:
function useUpdateProduct() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: { id: string; updates: Partial<Product> }) =>
api.products.update(data.id, data.updates),
onSuccess: (_, variables) => {
queryClient.invalidateQueries({ queryKey: productKeys.detail(variables.id) });
queryClient.invalidateQueries({ queryKey: productKeys.lists() });
},
});
}
Not this:
// Manual refetch after mutation — misses other components using same data
const { refetch } = useProduct(id);
await updateProduct(id, data);
await refetch();
Common Patterns
Optimistic Updates
function useToggleFavorite() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (productId: string) => api.products.toggleFavorite(productId),
onMutate: async (productId) => {
await queryClient.cancelQueries({ queryKey: productKeys.detail(productId) });
const previous = queryClient.getQueryData<Product>(productKeys.detail(productId));
queryClient.setQueryData<Product>(productKeys.detail(productId), (old) =>
old ? { ...old, isFavorite: !old.isFavorite } : old
);
return { previous };
},
onError: (_, productId, context) => {
queryClient.setQueryData(productKeys.detail(productId), context?.previous);
},
onSettled: (_, __, productId) => {
queryClient.invalidateQueries({ queryKey: productKeys.detail(productId) });
},
});
}
Prefetching on Hover
function ProductCard({ product }: { product: Product }) {
const queryClient = useQueryClient();
const prefetch = () => {
queryClient.prefetchQuery({
queryKey: productKeys.detail(product.id),
queryFn: () => api.products.getById(product.id),
staleTime: 5 * 60 * 1000,
});
};
return (
<Link href={`/products/${product.id}`} onMouseEnter={prefetch}>
{product.name}
</Link>
);
}
Infinite Scrolling
function useInfiniteProducts(category: string) {
return useInfiniteQuery({
queryKey: ["products", "infinite", category],
queryFn: ({ pageParam }) => api.products.list({ category, cursor: pageParam }),
initialPageParam: undefined as string | undefined,
getNextPageParam: (lastPage) => lastPage.nextCursor,
});
}
Anti-Patterns
- Copying query data into useState: Creates stale duplicates; use query data directly
- staleTime: Infinity without invalidation: Data never refreshes; pair with mutation invalidation
- Flat string query keys: Impossible to invalidate groups; use structured array keys
- Fetching in useEffect then setting state: Reimplements React Query poorly; use useQuery
When to Use
- Any React SPA that fetches data from REST or GraphQL APIs
- Dashboards and admin panels with frequent data updates
- E-commerce product listings with filters, pagination, and detail views
- Apps requiring optimistic updates for responsive interactions
- Pages where prefetching on hover or route transition improves perceived speed
Install this skill directly: skilldb add caching-services-skills
Related Skills
Apache Ignite
Integrate Apache Ignite, a high-performance, fault-tolerant distributed in-memory data grid.
Cloudflare Kv
Integrate Cloudflare Workers KV for globally distributed edge key-value storage.
Dragonfly
Integrate Dragonfly, a high-performance, in-memory data store compatible with Redis and Memcached APIs.
Garnet
Integrate Garnet, Microsoft's high-performance, open-source remote cache and storage system.
Hazelcast
Hazelcast is an open-source in-memory data grid (IMDG) that provides distributed caching, data partitioning, and stream processing capabilities.
Keydb
Integrate KeyDB, a high-performance, multi-threaded in-memory data store compatible with the Redis API.