TanStack Query is a library for managing server state in React applications.
It helps with:
npm install @tanstack/react-querynpm install @tanstack/react-query-devtools
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";import { ReactQueryDevtools } from "@tanstack/react-query-devtools";const queryClient = new QueryClient();export function App() {return (<QueryClientProvider client={queryClient}><App /><ReactQueryDevtools initialIsOpen={false} /></QueryClientProvider>);}
import { useQuery } from "@tanstack/react-query";const fetchPokemon = async (name: string) => {const res = await fetch(`https://pokeapi.co/api/v2/pokemon/${name}`);return res.json();};function PokemonDetail({ name }: { name: string }) {const { data, isLoading, error } = useQuery({queryKey: ["pokemon", name],queryFn: () => fetchPokemon(name),enabled: !!name,});if (isLoading) return <p>Loading...</p>;if (error) return <p>Error</p>;return <div>{data.name}</div>;}
["pokemon", name]["users", { page, limit }]
Rules:
| Option | Meaning |
|---|---|
staleTime | How long data is considered fresh |
gcTime | (v5) Replaces cacheTime |
enabled | Conditional fetching |
refetchOnWindowFocus | Auto refetch on focus |
POST / PUT / DELETE
import { useMutation, useQueryClient } from "@tanstack/react-query";const createTodo = (todo) =>fetch("/api/todos", {method: "POST",body: JSON.stringify(todo),});function AddTodo() {const queryClient = useQueryClient();const mutation = useMutation({mutationFn: createTodo,onSuccess: () => {queryClient.invalidateQueries({ queryKey: ["todos"] });},});return (<button onClick={() => mutation.mutate({ title: "New todo" })}>Add Todo</button>);}
queryClient.invalidateQueries({ queryKey: ["todos"] });// Marks data as stale (recommended)queryClient.refetchQueries({ queryKey: ["todos"] });// Immediately refetches
✅ Best practice: use invalidateQueries
Infinite Scrolling / Pagination
import { useInfiniteQuery } from "@tanstack/react-query";const fetchPokemons = ({ pageParam = 0 }) =>fetch(`https://pokeapi.co/api/v2/pokemon?offset=${pageParam}&limit=20`).then(res => res.json());const {data,fetchNextPage,hasNextPage,} = useInfiniteQuery({queryKey: ["pokemon-list"],queryFn: fetchPokemons,getNextPageParam: (lastPage) =>lastPage.next ? lastPage.offset + 20 : undefined,});
Show previous data while fetching new data
useQuery({queryKey: ["pokemon", name],queryFn: () => fetchPokemon(name),placeholderData: (previousData) => previousData,});
Set initial data on first load
useQuery({queryKey: ["todos"],queryFn: fetchTodos,initialData: [],});
Customize retry logic
useQuery({queryKey: ["data"],queryFn: fetchData,retry: (failureCount, error) => {if (error.status === 404) return false;return failureCount < 2;},});
Access query key and page param in query function
useInfiniteQuery({queryKey: ["pokemon-list"],queryFn: ({ pageParam = 0 }) => fetchPokemons(pageParam),getNextPageParam: (lastPage) =>lastPage.next ? lastPage.offset + 20 : undefined,});