Skip to main content
Technology & EngineeringCaching Services198 lines

React Query Cache

Implement TanStack React Query for client-side caching and server state

Quick Summary25 lines
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 lines
Paste into your CLAUDE.md or agent config

TanStack 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

Get CLI access →