If you’ve built React applications that communicate with APIs, you’ve probably managed server state using tools like useState, useEffect, and sometimes Redux. But as your app grows, handling loading states, caching, background refetching, pagination, and error recovery can become complicated.
This is where React Query shines.
React Query is a powerful data-fetching and state management library designed specifically for server state. It simplifies fetching, caching, synchronizing, and updating data in your UI — without the boilerplate of Redux or manual useEffect calls.
Before React Query, typical data-fetching logic looked something like this:
const [data, setData] = useState(null);const [loading, setLoading] = useState(false);const [error, setError] = useState(null);useEffect(() => {setLoading(true);fetch('/api/users').then(res => res.json()).then(setData).catch(setError).finally(() => setLoading(false));}, []);
This approach works — but you’ll need to repeat similar logic across components and maintain state manually. It’s also tricky to handle things like:
React Query solves all of this with a clean, declarative API.
| Concept | Purpose |
|---|---|
| Query | Fetches server data (e.g., GET requests) |
| Mutation | Changes server data (e.g., POST, PUT, DELETE) |
| Query Key | A unique identifier for the data in the cache |
| Query Client | Central cache manager for React Query |
| Stale Time | Determines how long data is considered “fresh” |
| Cache Time | How long unused data stays in memory |
useQuery)Used for fetching data from the server.
import { useQuery } from '@tanstack/react-query';const { data, isLoading, isError, refetch } = useQuery({queryKey: ['todos'],queryFn: fetchTodos,});
| Property | Description |
|---|---|
data | The returned data from your API |
isLoading | True while the query is fetching |
isError | True if the query failed |
error | Error message if failed |
refetch() | Manually re-fetch the query |
| Option | Meaning |
|---|---|
staleTime | How long data stays “fresh” |
cacheTime | How long to keep unused data in cache |
enabled | If false, the query does not run automatically |
refetchOnWindowFocus | Re-fetch when browser tab refocuses |
Example:
useQuery({queryKey: ['pokemon'],queryFn: fetchPokemon,staleTime: 1000 * 60 * 5, // 5 minutesrefetchOnWindowFocus: false,});
useMutation)Used to modify data — create, update, delete.
import { useMutation, useQueryClient } from '@tanstack/react-query';const queryClient = useQueryClient();const mutation = useMutation({mutationFn: addTodo,onSuccess: () => {queryClient.invalidateQueries(['todos']); // refresh list},});
| Method | Description |
|---|---|
mutation.mutate() | Execute the mutation |
onSuccess | Called when request succeeds |
onError | Called when request fails |
invalidateQueries() | Re-fetch affected queries |
React Query stores all server state in a central cache using a Query Client.
import { QueryClient } from '@tanstack/react-query';const queryClient = new QueryClient();
| Method | Purpose |
|---|---|
invalidateQueries(queryKey) | Re-fetch queries now stale |
setQueryData(queryKey, newData) | Update cached data manually |
getQueryData(queryKey) | Read cached data |
Example (Optimistic UI update):
queryClient.setQueryData(['todos'], (old) => [...old, newItem]);
useInfiniteQuery)Used for pagination or infinite scroll.
import { useInfiniteQuery } from '@tanstack/react-query';const query = useInfiniteQuery({queryKey: ['pokemon'],queryFn: fetchPokemonPage,getNextPageParam: (lastPage) => lastPage.nextPageCursor,});
npm install @tanstack/react-query-devtools
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';<QueryClientProvider client={queryClient}><App /><ReactQueryDevtools initialIsOpen={false} /></QueryClientProvider>
This allows you to:
| Feature | Hook | Purpose |
|---|---|---|
| Fetch data | useQuery | GET requests |
| Load more pages | useInfiniteQuery | Paginated data |
| Modify data | useMutation | POST / PUT / DELETE |
| Access cache | useQueryClient | Invalidate or update queries |
| Inspect cache | ReactQueryDevtools | Debug UI |
React Query replaces manual API handling with a predictable, maintainable pattern.
Once you understand useQuery, useMutation, and cache operations, you can build complex, scalable data flows easily.
If you’d like, I can now provide:
✅ Visual diagrams
✅ Example for Mutations (Create / Update / Delete Pokémon)
✅ Infinite Scroll Pokémon List
✅ Detail Page with useQuery + dynamic params
Just say: “Continue with Pokémon mutation example” 🧩