TanStack Query Complete Guide | React Query· Data Fetching
이 글의 핵심
TanStack Query (React Query) simplifies data fetching in React apps with automatic caching, background updates, and smart refetching. This guide covers everything from setup to advanced patterns.
Introduction
TanStack Query (formerly React Query) is a powerful data-fetching library that handles server state management in React applications. It provides automatic caching, background updates, and a clean API that eliminates most boilerplate code.
Used by thousands of production apps including those at Vercel, Shopify, and major startups, TanStack Query has become the de facto standard for server state management in React. This guide covers everything from basic setup to advanced patterns you’ll use in real projects.
Why TanStack Query?
Traditional data fetching with useEffect and useState requires managing loading states, error handling, caching, refetching, and deduplication manually. TanStack Query handles all of this automatically.
Before TanStack Query:
// Manual state management - 15+ lines per fetch
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));
}, []);
// Plus: no caching, no background refetch, no deduplication
With TanStack Query:
// Single line - handles everything automatically
const { data, isLoading, error } = useQuery({
queryKey: ['users'],
queryFn: () => fetch('/api/users').then(res => res.json())
});
// Includes: automatic caching, background refetch, request deduplication
What TanStack Query Provides
- Automatic caching ??fetch once, reuse everywhere
- Background refetching ??keep data fresh without blocking UI
- Request deduplication ??multiple components requesting same data? Only one network call
- Optimistic updates ??instant UI feedback before server responds
- Infinite queries ??load more pagination made easy
- DevTools ??inspect queries, mutations, and cache in real time
1. Installation & Setup
Install Dependencies
npm install @tanstack/react-query
npm install -D @tanstack/react-query-devtools
Configure QueryClient
// app/providers.tsx
'use client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { useState } from 'react';
export default function Providers({ children }: { children: React.ReactNode }) {
const [queryClient] = useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000, // 1 minute
cacheTime: 5 * 60 * 1000, // 5 minutes
retry: 1,
refetchOnWindowFocus: false,
},
},
})
);
return (
<QueryClientProvider client={queryClient}>
{children}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}
// app/layout.tsx
import Providers from './providers';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}
2. useQuery: Fetching Data
Basic Query
import { useQuery } from '@tanstack/react-query';
interface User {
id: number;
name: string;
email: string;
}
function UserList() {
const { data, isLoading, error, isFetching } = useQuery<User[]>({
queryKey: ['users'],
queryFn: async () => {
const response = await fetch('/api/users');
if (!response.ok) {
throw new Error('Failed to fetch users');
}
return response.json();
},
});
if (isLoading) return <div>Loading users...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
{isFetching && <div>Updating...</div>}
<ul>
{data?.map((user) => (
<li key={user.id}>
{user.name} - {user.email}
</li>
))}
</ul>
</div>
);
}
Query with Parameters
function UserProfile({ userId }: { userId: number }) {
const { data: user } = useQuery({
queryKey: ['user', userId],
queryFn: async () => {
const res = await fetch(`/api/users/${userId}`);
return res.json();
},
enabled: !!userId, // Only run if userId exists
});
return <div>{user?.name}</div>;
}
Dependent Queries
function UserPosts({ userId }: { userId: number }) {
// First query
const { data: user } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetch(`/api/users/${userId}`).then(res => res.json()),
});
// Dependent query - only runs after user is loaded
const { data: posts } = useQuery({
queryKey: ['posts', user?.id],
queryFn: () => fetch(`/api/posts?userId=${user.id}`).then(res => res.json()),
enabled: !!user,
});
return <div>{/* Render posts */}</div>;
}
3. useMutation: Modifying Data
Basic Mutation
import { useMutation, useQueryClient } from '@tanstack/react-query';
interface CreateUserData {
name: string;
email: string;
}
function CreateUser() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: async (newUser: CreateUserData) => {
const response = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newUser),
});
return response.json();
},
onSuccess: () => {
// Invalidate and refetch
queryClient.invalidateQueries({ queryKey: ['users'] });
},
});
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
mutation.mutate({
name: formData.get('name') as string,
email: formData.get('email') as string,
});
};
return (
<form onSubmit={handleSubmit}>
<input name="name" placeholder="Name" />
<input name="email" placeholder="Email" />
<button type="submit" disabled={mutation.isPending}>
{mutation.isPending ? 'Creating...' : 'Create User'}
</button>
{mutation.isError && <div>Error: {mutation.error.message}</div>}
</form>
);
}
Optimistic Updates
function TodoList() {
const queryClient = useQueryClient();
const toggleMutation = useMutation({
mutationFn: async ({ id, completed }: { id: number; completed: boolean }) => {
const res = await fetch(`/api/todos/${id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ completed }),
});
return res.json();
},
onMutate: async ({ id, completed }) => {
// Cancel outgoing queries
await queryClient.cancelQueries({ queryKey: ['todos'] });
// Snapshot previous value
const previousTodos = queryClient.getQueryData(['todos']);
// Optimistically update
queryClient.setQueryData(['todos'], (old: any) =>
old.map((todo: any) =>
todo.id === id ? { ...todo, completed } : todo
)
);
return { previousTodos };
},
onError: (err, variables, context) => {
// Rollback on error
queryClient.setQueryData(['todos'], context?.previousTodos);
},
onSettled: () => {
// Refetch after mutation
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
});
return <div>{/* Render todos */}</div>;
}
4. Infinite Queries
Pagination
function InfiniteUserList() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useInfiniteQuery({
queryKey: ['users'],
queryFn: async ({ pageParam = 1 }) => {
const res = await fetch(`/api/users?page=${pageParam}&limit=20`);
return res.json();
},
getNextPageParam: (lastPage, allPages) => {
return lastPage.hasMore ? allPages.length + 1 : undefined;
},
initialPageParam: 1,
});
return (
<div>
{data?.pages.map((page, i) => (
<div key={i}>
{page.users.map((user: any) => (
<div key={user.id}>{user.name}</div>
))}
</div>
))}
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
>
{isFetchingNextPage
? 'Loading more...'
: hasNextPage
? 'Load More'
: 'No more data'}
</button>
</div>
);
}
5. Advanced Patterns
Query Invalidation
const queryClient = useQueryClient();
// Invalidate specific query
queryClient.invalidateQueries({ queryKey: ['users'] });
// Invalidate all queries starting with 'users'
queryClient.invalidateQueries({ queryKey: ['users'], exact: false });
// Invalidate all queries
queryClient.invalidateQueries();
Prefetching
function UserListWithPrefetch() {
const queryClient = useQueryClient();
const handleMouseEnter = (userId: number) => {
queryClient.prefetchQuery({
queryKey: ['user', userId],
queryFn: () => fetch(`/api/users/${userId}`).then(res => res.json()),
});
};
return (
<ul>
{users.map((user) => (
<li key={user.id} onMouseEnter={() => handleMouseEnter(user.id)}>
<Link to={`/users/${user.id}`}>{user.name}</Link>
</li>
))}
</ul>
);
}
Custom Hooks
// hooks/useUsers.ts
export function useUsers() {
return useQuery({
queryKey: ['users'],
queryFn: async () => {
const res = await fetch('/api/users');
return res.json();
},
});
}
export function useCreateUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (data: CreateUserData) => {
const res = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
return res.json();
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] });
},
});
}
6. Configuration Options
Query Options
useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
// Caching
staleTime: 5 * 60 * 1000, // 5 minutes
cacheTime: 10 * 60 * 1000, // 10 minutes
// Refetching
refetchOnMount: true,
refetchOnWindowFocus: false,
refetchOnReconnect: true,
refetchInterval: false,
// Retry
retry: 3,
retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000),
// Conditional
enabled: true,
});
7. Error Handling
Global Error Handler
const queryClient = new QueryClient({
defaultOptions: {
queries: {
onError: (error) => {
console.error('Query error:', error);
toast.error('Failed to fetch data');
},
},
mutations: {
onError: (error) => {
console.error('Mutation error:', error);
toast.error('Failed to update data');
},
},
},
});
Per-Query Error Handling
const { data, error } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
onError: (error) => {
if (error instanceof Error) {
toast.error(error.message);
}
},
});
8. TypeScript Integration
import { useQuery, useMutation, type UseQueryResult } from '@tanstack/react-query';
interface User {
id: number;
name: string;
email: string;
}
interface ApiError {
message: string;
code: string;
}
// Typed query
function useUser(userId: number): UseQueryResult<User, ApiError> {
return useQuery({
queryKey: ['user', userId],
queryFn: async () => {
const res = await fetch(`/api/users/${userId}`);
if (!res.ok) {
throw { message: 'Failed to fetch', code: 'FETCH_ERROR' };
}
return res.json();
},
});
}
9. Best Practices
1. Use Query Keys Consistently
// ??Bad
queryKey: ['users', userId, 'posts']
// ??Good
queryKey: ['users', userId, 'posts'] as const
2. Create Custom Hooks
// Reusable, testable, maintainable
export const useUsers = () => useQuery({...});
export const useCreateUser = () => useMutation({...});
3. Handle Loading States
if (isLoading) return <Skeleton />;
if (error) return <ErrorMessage error={error} />;
if (!data) return null;
4. Use Optimistic Updates for Better UX
// Immediate feedback, rollback on error
onMutate: async (newData) => {
await queryClient.cancelQueries({ queryKey: ['todos'] });
const previous = queryClient.getQueryData(['todos']);
queryClient.setQueryData(['todos'], (old) => [...old, newData]);
return { previous };
},
10. Performance Tips
Reduce Re-renders
// ??Re-renders on every state change
const { data, isLoading, error, isFetching } = useQuery({...});
// ??Only re-render when needed
const { data } = useQuery({...});
Selective Refetching
// Only refetch when needed
queryClient.invalidateQueries({
queryKey: ['users', userId],
exact: true
});
Use Suspense (Experimental)
const { data } = useSuspenseQuery({
queryKey: ['users'],
queryFn: fetchUsers,
});
// No need for isLoading check - Suspense handles it
Summary
TanStack Query transforms data fetching in React:
- Automatic caching reduces network requests
- Background refetching keeps data fresh
- Optimistic updates improve perceived performance
- DevTools make debugging easy
Key Takeaways:
- Replace useEffect + fetch with useQuery
- Use queryKey for automatic caching
- Implement optimistic updates for better UX
- Create custom hooks for reusability
- Configure staleTime and cacheTime appropriately
Next Steps:
- Explore [tRPC integration](/en/blog/trpc-complete-guide/ for type-safe APIs
- Learn [Zustand](/en/blog/zustand-complete-guide/ for client state
- Check [React 18 features](/en/blog/react-18-deep-dive/
Resources:
Real-World Use Cases
When to Use TanStack Query
TanStack Query excels in applications that:
- Fetch data from REST APIs frequently ??dashboards, admin panels, social feeds
- Need real-time updates ??chat apps, collaborative tools, live dashboards
- Display server data in multiple components ??shared state across the app
- Require offline support ??the cache can serve stale data when offline
- Need optimistic UI updates ??instant feedback before server confirmation
When NOT to Use TanStack Query
- Pure client-side state ??use Zustand, Jotai, or Context for UI state
- Static content ??SSG with Next.js is better for blogs, marketing sites
- GraphQL-heavy apps ??consider Apollo Client or urql instead
- WebSocket/real-time streams ??TanStack Query works but specialized libraries may fit better
Production Example: Dashboard
Here’s how a production dashboard might structure its queries:
// hooks/useDashboard.ts
export function useDashboardData() {
const { data: stats } = useQuery({
queryKey: ['dashboard', 'stats'],
queryFn: fetchDashboardStats,
staleTime: 30 * 1000, // 30 seconds
refetchInterval: 60 * 1000, // Refresh every minute
});
const { data: activities } = useQuery({
queryKey: ['dashboard', 'activities'],
queryFn: fetchRecentActivities,
staleTime: 10 * 1000,
});
return { stats, activities };
}
// components/Dashboard.tsx
export default function Dashboard() {
const { stats, activities } = useDashboardData();
return (
<div>
<StatsCards data={stats} />
<ActivityFeed data={activities} />
</div>
);
}
This pattern:
- Separates data fetching logic from UI
- Enables independent refetch intervals
- Makes components testable
- Shares cache across the app automatically
Related Reading
Continue learning React patterns:
- [SWR Complete Guide](/en/blog/swr-complete-guide/ ??Alternative to TanStack Query by Vercel
- [React 18 Deep Dive](/en/blog/react-18-deep-dive/ ??Concurrent rendering and Suspense
- [Zustand Complete Guide](/en/blog/zustand-complete-guide/ ??Lightweight client state management
Keywords
TanStack Query, React Query, Data Fetching, Cache, Server State, Mutations, Optimistic Updates, React Hooks, TypeScript