Tanstack Query: Modern Data Fetching in React

Learn how Tanstack Query revolutionizes async state management and why it might be all you need for most React applications.

Web Apps
4 min read

Remember the days of manually managing loading states, caching API responses, and handling errors in React applications? If you're still doing that, you're about to discover how Tanstack Query (formerly React Query) transforms the way we handle server state in modern React applications.

Why Tanstack Query Changes Everything#

Think of Tanstack Query as your personal assistant for managing all things async in your React applications. It's not just another state management library – it's a powerful tool that handles the complexities of server state so you don't have to.

Let's look at a real-world example. Here's how you might fetch a list of todos without Tanstack Query:

1function TodoList() {
2 const [todos, setTodos] = useState([])
3 const [isLoading, setIsLoading] = useState(true)
4 const [error, setError] = useState(null)
5
6 useEffect(() => {
7 fetchTodos()
8 .then(data => setTodos(data))
9 .catch(err => setError(err))
10 .finally(() => setIsLoading(false))
11 }, [])
12
13 if (isLoading) return <div>Loading...</div>
14 if (error) return <div>Error: {error.message}</div>
15
16 return (
17 <ul>
18 {todos.map(todo => (
19 <li key={todo.id}>{todo.title}</li>
20 ))}
21 </ul>
22 )
23}
1function TodoList() {
2 const [todos, setTodos] = useState([])
3 const [isLoading, setIsLoading] = useState(true)
4 const [error, setError] = useState(null)
5
6 useEffect(() => {
7 fetchTodos()
8 .then(data => setTodos(data))
9 .catch(err => setError(err))
10 .finally(() => setIsLoading(false))
11 }, [])
12
13 if (isLoading) return <div>Loading...</div>
14 if (error) return <div>Error: {error.message}</div>
15
16 return (
17 <ul>
18 {todos.map(todo => (
19 <li key={todo.id}>{todo.title}</li>
20 ))}
21 </ul>
22 )
23}

Now, here's the same component with Tanstack Query:

1function TodoList() {
2 const { data, isLoading, error } = useQuery({
3 queryKey: ['todos'],
4 queryFn: fetchTodos
5 })
6
7 if (isLoading) return <div>Loading...</div>
8 if (error) return <div>Error: {error.message}</div>
9
10 return (
11 <ul>
12 {data.map(todo => (
13 <li key={todo.id}>{todo.title}</li>
14 ))}
15 </ul>
16 )
17}
1function TodoList() {
2 const { data, isLoading, error } = useQuery({
3 queryKey: ['todos'],
4 queryFn: fetchTodos
5 })
6
7 if (isLoading) return <div>Loading...</div>
8 if (error) return <div>Error: {error.message}</div>
9
10 return (
11 <ul>
12 {data.map(todo => (
13 <li key={todo.id}>{todo.title}</li>
14 ))}
15 </ul>
16 )
17}

The difference is striking. Not only is the code more concise, but Tanstack Query is also handling caching, background updates, and error retries behind the scenes.

Real-World Application#

Let's explore how Tanstack Query handles common real-world scenarios:

Optimistic Updates#

Imagine updating a todo item. Instead of waiting for the server response, we can update the UI immediately:

1const queryClient = useQueryClient()
2
3const mutation = useMutation({
4 mutationFn: updateTodo,
5 onMutate: async (newTodo) => {
6 // Cancel outgoing refetches
7 await queryClient.cancelQueries({ queryKey: ['todos'] })
8
9 // Snapshot the previous value
10 const previousTodos = queryClient.getQueryData(['todos'])
11
12 // Optimistically update to the new value
13 queryClient.setQueryData(['todos'], old =>
14 old.map(todo => todo.id === newTodo.id ? newTodo : todo)
15 )
16
17 return { previousTodos }
18 },
19 onError: (err, newTodo, context) => {
20 // If the mutation fails, roll back
21 queryClient.setQueryData(['todos'], context.previousTodos)
22 }
23})
1const queryClient = useQueryClient()
2
3const mutation = useMutation({
4 mutationFn: updateTodo,
5 onMutate: async (newTodo) => {
6 // Cancel outgoing refetches
7 await queryClient.cancelQueries({ queryKey: ['todos'] })
8
9 // Snapshot the previous value
10 const previousTodos = queryClient.getQueryData(['todos'])
11
12 // Optimistically update to the new value
13 queryClient.setQueryData(['todos'], old =>
14 old.map(todo => todo.id === newTodo.id ? newTodo : todo)
15 )
16
17 return { previousTodos }
18 },
19 onError: (err, newTodo, context) => {
20 // If the mutation fails, roll back
21 queryClient.setQueryData(['todos'], context.previousTodos)
22 }
23})

Infinite Loading#

Building an infinite scroll list? Tanstack Query makes it surprisingly simple:

1function InfiniteList() {
2 const {
3 data,
4 fetchNextPage,
5 hasNextPage,
6 isFetchingNextPage
7 } = useInfiniteQuery({
8 queryKey: ['posts'],
9 queryFn: fetchPostPage,
10 getNextPageParam: (lastPage) => lastPage.nextCursor
11 })
12
13 return (
14 <div>
15 {data.pages.map((page) => (
16 page.posts.map((post) => (
17 <Post key={post.id} post={post} />
18 ))
19 ))}
20
21 <button
22 onClick={() => fetchNextPage()}
23 disabled={!hasNextPage || isFetchingNextPage}
24 >
25 {isFetchingNextPage
26 ? 'Loading more...'
27 : hasNextPage
28 ? 'Load more'
29 : 'Nothing more to load'}
30 </button>
31 </div>
32 )
33}
1function InfiniteList() {
2 const {
3 data,
4 fetchNextPage,
5 hasNextPage,
6 isFetchingNextPage
7 } = useInfiniteQuery({
8 queryKey: ['posts'],
9 queryFn: fetchPostPage,
10 getNextPageParam: (lastPage) => lastPage.nextCursor
11 })
12
13 return (
14 <div>
15 {data.pages.map((page) => (
16 page.posts.map((post) => (
17 <Post key={post.id} post={post} />
18 ))
19 ))}
20
21 <button
22 onClick={() => fetchNextPage()}
23 disabled={!hasNextPage || isFetchingNextPage}
24 >
25 {isFetchingNextPage
26 ? 'Loading more...'
27 : hasNextPage
28 ? 'Load more'
29 : 'Nothing more to load'}
30 </button>
31 </div>
32 )
33}

Best Practices for Success#

After working with Tanstack Query in production, here are some key practices that will serve you well:

  1. Structure Your Queries Create custom hooks for your queries to encapsulate the logic and make it reusable:
1export function useTodos() {
2 return useQuery({
3 queryKey: ['todos'],
4 queryFn: fetchTodos,
5 staleTime: 5 * 60 * 1000, // Consider data fresh for 5 minutes
6 select: (data) => data.sort((a, b) => b.id - a.id)
7 })
8}
1export function useTodos() {
2 return useQuery({
3 queryKey: ['todos'],
4 queryFn: fetchTodos,
5 staleTime: 5 * 60 * 1000, // Consider data fresh for 5 minutes
6 select: (data) => data.sort((a, b) => b.id - a.id)
7 })
8}
  1. Handle Loading States Gracefully Create consistent loading states across your application:
1function LoadingState({ isLoading, error, children }) {
2 if (isLoading) return <Spinner />
3 if (error) return <ErrorMessage error={error} />
4 return children
5}
1function LoadingState({ isLoading, error, children }) {
2 if (isLoading) return <Spinner />
3 if (error) return <ErrorMessage error={error} />
4 return children
5}
  1. Prefetch Data Improve user experience by prefetching data before it's needed:
1const prefetchTodo = async (id) => {
2 await queryClient.prefetchQuery({
3 queryKey: ['todo', id],
4 queryFn: () => fetchTodoById(id)
5 })
6}
1const prefetchTodo = async (id) => {
2 await queryClient.prefetchQuery({
3 queryKey: ['todo', id],
4 queryFn: () => fetchTodoById(id)
5 })
6}

When to Use Tanstack Query#

Tanstack Query shines brightest when:

  • You're working with server state
  • You need automatic background updates
  • Caching and synchronization are important
  • You want to optimize network requests

It's particularly valuable in applications that:

  • Display real-time or frequently updated data
  • Require optimistic updates for better UX
  • Need sophisticated cache management
  • Handle complex data fetching scenarios

The Bottom Line#

Tanstack Query isn't just another library – it's a paradigm shift in how we handle server state in React applications. It eliminates countless lines of boilerplate code while providing a more robust and maintainable solution.

The best part? It works seamlessly with your existing state management solution. Use Redux or Zustand for client state, and let Tanstack Query handle all your server state needs. It's a powerful combination that will serve your application well.

Remember: The goal isn't just to write less code – it's to write more maintainable, efficient, and user-friendly applications. Tanstack Query helps you achieve exactly that.