TanStack Query (React Query) in React + Vite + TypeScript

This guide provides a production-ready architectural pattern for implementing TanStack Query in a React application. It focuses on scalability, type safety, and maintainability.

Install via CLI
$ npx @airuleshub/cli@latest add tanstack-query-react-query-in-react-vite-typescript

Rule Content

The Guide to TanStack Query (React Query) in React + Vite + TypeScript

This guide provides a production-ready architectural pattern for implementing TanStack Query in a React application. It focuses on scalability, type safety, and maintainability.

1. Installation & Setup

First, install the core package and the devtools.

bash
npm install @tanstack/react-query @tanstack/react-query-devtools axios

Global Configuration (src/main.tsx)

Wrap your application in QueryClientProvider.

tsx
import { StrictMode } from 'react';import { createRoot } from 'react-dom/client';import { QueryClient, QueryClientProvider } from '@tanstack/react-query';import { ReactQueryDevtools } from '@tanstack/react-query-devtools';import App from './App';
// Create a clientconst queryClient = new QueryClient({  defaultOptions: {    queries: {      staleTime: 1000 * 60 * 5, // Data is fresh for 5 minutes      gcTime: 1000 * 60 * 60, // Garbage collect unused data after 1 hour      retry: 1, // Retry failed requests once      refetchOnWindowFocus: false, // Prevent refetching on window focus (optional)    },  },});
createRoot(document.getElementById('root')!).render(  <StrictMode>    <QueryClientProvider client={queryClient}>      <App />      {/* DevTools are essential for debugging */}      <ReactQueryDevtools initialIsOpen={false} />    </QueryClientProvider>  </StrictMode>);

2. Recommended Directory Structure

Isolate your data fetching logic from your UI components.

src/├── api/                # Pure Axios/Fetch functions│   ├── axios-client.ts # configured axios instance│   └── todos.api.ts    # API methods for a specific feature├── hooks/              # Custom React Query hooks│   └── queries/        # Group hooks by feature│       └── useTodos.ts├── types/              # TypeScript interfaces│   └── todo.types.ts└── lib/    └── query-keys.ts   # Centralized query keys

3. Query Key Factory Pattern (src/lib/query-keys.ts)

Crucial: Never hardcode query keys (strings/arrays) directly in your components. Use a factory to maintain consistency.

typescript
// src/lib/query-keys.tsexport const todoKeys = {  all: ['todos'] as const,  lists: () => [...todoKeys.all, 'list'] as const,  list: (filters: string) => [...todoKeys.lists(), { filters }] as const,  details: () => [...todoKeys.all, 'detail'] as const,  detail: (id: number) => [...todoKeys.details(), id] as const,};

4. The API Layer (src/api/*)

Keep API calls strictly as pure functions returning Promises. They should know nothing about React.

typescript
// src/api/todos.api.tsimport axios from 'axios';import { Todo } from '../types/todo.types';
const api = axios.create({ baseURL: 'https://jsonplaceholder.typicode.com' });
export const fetchTodos = async (): Promise<Todo[]> => {  const { data } = await api.get('/todos');  return data;};
export const fetchTodoById = async (id: number): Promise<Todo> => {  const { data } = await api.get(`/todos/${id}`);  return data;};
export const createTodo = async (newTodo: Omit<Todo, 'id'>): Promise<Todo> => {  const { data } = await api.post('/todos', newTodo);  return data;};

5. Custom Hooks (The "Glue" Layer)

Wrap useQuery and useMutation in custom hooks. This gives you a single place to handle key management, types, and default options.

Fetching Data (useQuery)

typescript
// src/hooks/queries/useTodos.tsimport { useQuery } from '@tanstack/react-query';import { todoKeys } from '../../lib/query-keys';import { fetchTodos, fetchTodoById } from '../../api/todos.api';
export const useTodos = () => {  return useQuery({    queryKey: todoKeys.lists(),    queryFn: fetchTodos,    staleTime: 1000 * 60, // 1 minute specific stale time for this query  });};
export const useTodo = (id: number) => {  return useQuery({    queryKey: todoKeys.detail(id),    queryFn: () => fetchTodoById(id),    enabled: !!id, // Only run if ID is present  });};

Mutating Data (useMutation)

Always invalidate related queries on success to keep the UI in sync.

typescript
// src/hooks/queries/useTodos.tsimport { useMutation, useQueryClient } from '@tanstack/react-query';import { createTodo } from '../../api/todos.api';import { todoKeys } from '../../lib/query-keys';
export const useCreateTodo = () => {  const queryClient = useQueryClient();
  return useMutation({    mutationFn: createTodo,    onSuccess: () => {      // Invalidate the 'lists' query so the new item appears      queryClient.invalidateQueries({ queryKey: todoKeys.lists() });    },    onError: (error) => {      console.error('Failed to create todo:', error);    },  });};

6. Usage in Components

Components become clean and focused purely on UI logic.

tsx
// src/components/TodoList.tsximport { useTodos, useCreateTodo } from '../hooks/queries/useTodos';
export const TodoList = () => {  const { data: todos, isLoading, isError } = useTodos();  const createMutation = useCreateTodo();
  if (isLoading) return <div>Loading...</div>;  if (isError) return <div>Error fetching todos</div>;
  const handleCreate = () => {    createMutation.mutate({ title: 'New Task', completed: false, userId: 1 });  };
  return (    <div>      <button onClick={handleCreate} disabled={createMutation.isPending}>        {createMutation.isPending ? 'Adding...' : 'Add Todo'}      </button>
      <ul>        {todos?.map((todo) => (          <li key={todo.id}>{todo.title}</li>        ))}      </ul>    </div>  );};

7. Advanced Patterns

Optimistic Updates

Update the UI immediately before the server responds.

typescript
export const useUpdateTodo = () => {  const queryClient = useQueryClient();
  return useMutation({    mutationFn: updateTodoApi,    onMutate: async (newTodo) => {      // 1. Cancel outgoing refetches      await queryClient.cancelQueries({ queryKey: todoKeys.detail(newTodo.id) });
      // 2. Snapshot previous value      const previousTodo = queryClient.getQueryData(todoKeys.detail(newTodo.id));
      // 3. Optimistically update      queryClient.setQueryData(todoKeys.detail(newTodo.id), (old: any) => ({        ...old,        ...newTodo,      }));
      // 4. Return context      return { previousTodo };    },    onError: (err, newTodo, context) => {      // 5. Rollback on error      queryClient.setQueryData(todoKeys.detail(newTodo.id), context?.previousTodo);    },    onSettled: (newTodo) => {      // 6. Refetch to ensure server sync      queryClient.invalidateQueries({ queryKey: todoKeys.detail(newTodo?.id) });    },  });};

Type-Safe Error Handling

Define a global error type for your API.

typescript
// types/api.types.tsexport interface ApiError {  message: string;  statusCode: number;}
// In custom hookimport { useQuery, UseQueryOptions } from '@tanstack/react-query';import { ApiError } from '../types/api.types';
export const useTodos = (options?: UseQueryOptions<Todo[], ApiError>) => {  return useQuery({    queryKey: todoKeys.lists(),    queryFn: fetchTodos,    ...options,  });};

8. Best Practices Checklist

  • Global Stale Time: Set a reasonable global staleTime (e.g., 5 mins) to avoid excessive background fetching.
  • Query Key Factories: Always use a factory/object for keys (todoKeys.list(filter)).
  • Separation of Concerns: Component -> Custom Hook -> API Function.
  • Invalidation: Always invalidate parent lists when creating/deleting items.
  • DevTools: Keep ReactQueryDevtools in your app wrapper (it's stripped in production).

Command Palette

Search for a command to run...