Documentation
State Management
TanStack Query

TanStack Query

Server state management and data fetching.

Query Keys

Organize query keys in a centralized file:

// constants/queryKeys.ts
export 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,
  },
  posts: {
    all: ['posts'] as const,
    list: (filters?: PostFilters) => [...queryKeys.posts.all, 'list', filters] as const,
    detail: (id: string) => [...queryKeys.posts.all, 'detail', id] as const,
  },
};

Query Hooks

Create custom hooks for each query:

// features/users/hooks/useUsers.ts
import { useQuery } from '@tanstack/react-query';
import { queryKeys } from '@/constants/queryKeys';
import { userService } from '../services/userService';
 
export const useUsers = (filters?: UserFilters) => {
  return useQuery({
    queryKey: queryKeys.users.list(filters ?? {}),
    queryFn: () => userService.getUsers(filters),
  });
};
 
export const useUser = (id: string) => {
  return useQuery({
    queryKey: queryKeys.users.detail(id),
    queryFn: () => userService.getUser(id),
    enabled: !!id, // Only fetch when id exists
  });
};

Mutation Hooks

// features/users/hooks/useCreateUser.ts
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { queryKeys } from '@/constants/queryKeys';
import { userService } from '../services/userService';
 
export const useCreateUser = () => {
  const queryClient = useQueryClient();
 
  return useMutation({
    mutationFn: (data: CreateUserDTO) => userService.createUser(data),
    onSuccess: () => {
      // Invalidate and refetch
      queryClient.invalidateQueries({
        queryKey: queryKeys.users.lists(),
      });
    },
  });
};

Optimistic Updates

export const useUpdateUser = () => {
  const queryClient = useQueryClient();
 
  return useMutation({
    mutationFn: ({ id, data }: { id: string; data: UpdateUserDTO }) =>
      userService.updateUser(id, data),
 
    onMutate: async ({ id, data }) => {
      // Cancel outgoing refetches
      await queryClient.cancelQueries({
        queryKey: queryKeys.users.detail(id),
      });
 
      // Snapshot previous value
      const previousUser = queryClient.getQueryData(
        queryKeys.users.detail(id)
      );
 
      // Optimistically update
      queryClient.setQueryData(
        queryKeys.users.detail(id),
        (old: User) => ({ ...old, ...data })
      );
 
      return { previousUser };
    },
 
    onError: (err, { id }, context) => {
      // Rollback on error
      queryClient.setQueryData(
        queryKeys.users.detail(id),
        context?.previousUser
      );
    },
 
    onSettled: (data, error, { id }) => {
      // Always refetch after error or success
      queryClient.invalidateQueries({
        queryKey: queryKeys.users.detail(id),
      });
    },
  });
};

Delete with Optimistic Update

export const useDeleteUser = () => {
  const queryClient = useQueryClient();
 
  return useMutation({
    mutationFn: (id: string) => userService.deleteUser(id),
 
    onMutate: async (id) => {
      await queryClient.cancelQueries({
        queryKey: queryKeys.users.lists(),
      });
 
      const previousUsers = queryClient.getQueryData(
        queryKeys.users.lists()
      );
 
      queryClient.setQueryData(
        queryKeys.users.lists(),
        (old: User[] = []) => old.filter((user) => user.id !== id)
      );
 
      return { previousUsers };
    },
 
    onError: (err, id, context) => {
      queryClient.setQueryData(
        queryKeys.users.lists(),
        context?.previousUsers
      );
    },
 
    onSettled: () => {
      queryClient.invalidateQueries({
        queryKey: queryKeys.users.lists(),
      });
    },
  });
};

Query Options

// Stale time and cache time
export const useUsers = () => {
  return useQuery({
    queryKey: queryKeys.users.lists(),
    queryFn: userService.getUsers,
    staleTime: 5 * 60 * 1000,     // 5 minutes
    gcTime: 30 * 60 * 1000,       // 30 minutes (formerly cacheTime)
    refetchOnWindowFocus: false,
    retry: 3,
  });
};
 
// Dependent queries
export const useUserPosts = (userId: string) => {
  const { data: user } = useUser(userId);
 
  return useQuery({
    queryKey: ['users', userId, 'posts'],
    queryFn: () => postService.getPostsByUser(userId),
    enabled: !!user, // Only run when user is loaded
  });
};

Provider Setup

// app/providers/QueryProvider.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
 
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 60 * 1000, // 1 minute
      retry: 1,
      refetchOnWindowFocus: false,
    },
  },
});
 
export const QueryProvider = ({ children }: { children: ReactNode }) => (
  <QueryClientProvider client={queryClient}>
    {children}
    <ReactQueryDevtools initialIsOpen={false} />
  </QueryClientProvider>
);