Published

- 8 min read

Unlocking Superpowers with TanStack Query

img of Unlocking Superpowers with TanStack Query

Introduction: The “Aha!” Moment in Data Fetching

If you’ve ever built a React application, you’ve likely found yourself tangled in a web of useState and useEffect hooks just to fetch, display, and manage data from a server. We write endless boilerplate code to handle loading states, errors, and caching, only to realize it’s fragile and repetitive.

What if there was a better way? A way to treat data from our server not just as a one-time request, but as a living, breathing part of our application’s state?

This is where TanStack Query (formerly React Query) comes in. It’s not just another data-fetching library; it’s a fundamental shift in how we handle server state. This post will walk you through a detailed Q&A journey, starting from the basics and moving to production-ready configurations, to help you understand why this tool is considered a superpower in modern web development.


Q: Let’s start from the beginning. What’s wrong with the way we’ve always fetched data using useEffect and axios?

A: That’s the perfect place to start. To understand the solution, we must first appreciate the problem. Let’s use an analogy.

Imagine you’re a chef who needs apples for a pie. Using traditional methods (fetch or axios inside a useEffect) is like going to the greengrocer every single time you need an apple.

Your process in React looks something like this:

   import React, { useState, useEffect } from 'react'
import axios from 'axios'

function ApplePieComponent() {
	const [apples, setApples] = useState(null)
	const [isLoading, setIsLoading] = useState(true)
	const [error, setError] = useState(null)

	useEffect(() => {
		const getApples = async () => {
			try {
				setIsLoading(true) // Announce you're going to the store.
				const response = await axios.get('https://api.store.com/apples')
				setApples(response.data) // Put the apples on your kitchen counter.
			} catch (err) {
				setError(err) // Something went wrong on the way.
			} finally {
				setIsLoading(false) // You're back from the store.
			}
		}
		getApples()
	}, []) // Only go when the component first loads.

	if (isLoading) return <div>Walking to the greengrocer...</div>
	if (error) return <div>Oh no! There was a problem getting apples: {error.message}</div>

	return <div>I have fresh apples!</div>
}

This approach has several painful drawbacks:

  1. Repetitive Code (Boilerplate): For every single data request in your app, you have to manually manage data, isLoading, and error states with useState.
  2. No Memory (Caching): If another component also needs apples, it will make a brand new trip to the greengrocer, even if you just came back with a fresh batch. This means redundant network requests.
  3. Data Freshness is Your Problem: How do you know if the apples on your counter are still fresh? You don’t. To get fresh ones, you have to manually trigger another trip to the store.
  4. No Background Updates: You can’t sneak out to the store to check for fresher apples while the user is looking at the pie recipe.

In short, fetch and axios are just the car that takes you to the store. They don’t manage your pantry.


Q: So how does React Query change this? What does it do differently?

A: React Query isn’t the car; it’s the smart librarian. You don’t go to the store anymore. You ask the librarian for what you need, and they handle everything else.

When you ask the librarian for the “Apple Pie Recipes” book:

  • They go find it for you (isLoading: true).
  • They bring it to you (data).
  • If they can’t find it, they tell you (error).

But here’s where the magic happens:

  • Caching: The librarian keeps a copy of the book under their desk (cache). If you ask for the same book again moments later, they give you that copy instantly without going back to the shelves.
  • Stale-While-Revalidate: The librarian knows the copy under the desk might be slightly old. So, while they hand you the cached copy, they simultaneously go check the main shelf in the background to see if a newer edition has arrived. If it has, they silently swap it for the one on your table. You experience no loading spinner but still get the freshest data.

Here’s what our component looks like with the librarian’s help:

   import { useQuery } from '@tanstack/react-query'
import axios from 'axios'

const getApples = async () => {
	const { data } = await axios.get('https://api.store.com/apples')
	return data
}

function ApplePieComponentWithQuery() {
	// We ask the librarian for 'apples' and tell them how to get them (getApples).
	const { data, isLoading, isError, error } = useQuery({
		queryKey: ['apples'],
		queryFn: getApples
	})

	if (isLoading) return <div>The librarian is looking for apples...</div>
	if (isError) return <div>Oh no! There was a problem getting apples: {error.message}</div>

	return <div>The librarian brought me fresh apples!</div>
}

Notice how useState and useEffect vanished. All the complex state management is handled for us.

Q: Okay, but which assistant should I hire? Is the built-in fetch good enough, or do I really need axios?

A: You’re deciding between a standard helper and a fully-equipped professional.

1. fetch API (The Standard Helper)

It’s built into all modern browsers, so you don’t need to install anything.

  • Pros: Zero extra bundle size.
  • Cons (and these are big):
    • Bad Error Handling: This is the critical flaw. fetch does not consider a 404 (Not Found) or 500 (Server Error) to be an actual error. It will happily resolve the promise. You must manually check if (!response.ok) every single time, or React Query’s isError state will never trigger correctly.
    • Manual JSON Parsing: You always need a two-step process: await fetch(...) and then await response.json().

2. axios (The Professional)

A dedicated library for making HTTP requests.

  • Pros:
    • Sensible Error Handling: If the server returns a 4xx or 5xx error, axios automatically rejects the promise. This works perfectly with React Query’s error handling out of the box.
    • Automatic JSON Transformation: The response data is already parsed as JSON.
    • Interceptors: A killer feature. You can set up global “middleware” to automatically add an auth token to every request or handle all errors from a single location.
  • Cons: It adds a small amount to your project’s final bundle size.

Code Comparison

With fetch (more manual work):

   const getTodosWithFetch = async () => {
	const response = await fetch('/todos')

	// You MUST do this, or errors won't be caught!
	if (!response.ok) {
		throw new Error('Network response was not ok')
	}

	return response.json() // Second step for parsing
}

With axios (clean and robust):

   const getTodosWithAxios = async () => {
	const response = await axios.get('/todos')
	return response.data // Error handling and parsing are automatic.
}

Verdict: For any serious, production-ready application, the reliability and clean code offered by axios are almost always worth the tiny cost in bundle size. It makes your data-fetching layer far more robust and predictable.


Q: I’m sold. How do I set this up for a production-ready React or React Native app?

A: Great! Getting the setup right from the start will save you a lot of time.

1. The Global Setup (QueryClientProvider)

First, create a single QueryClient instance for your entire app. This is your central “librarian.” You configure its default behaviors here. Then, wrap your app in QueryClientProvider.

   // In your main App.js or index.js

import { QueryClient, QueryClientProvider } from '@tanstack/react-query'

// Create the client (the librarian)
const queryClient = new QueryClient({
	defaultOptions: {
		queries: {
			// How long data is considered "fresh" and won't be re-fetched.
			// After 5 minutes, it's "stale".
			staleTime: 5 * 60 * 1000, // 5 minutes

			// How long inactive data stays in the cache before being garbage collected.
			gcTime: 10 * 60 * 1000, // 10 minutes (must be >= staleTime)

			// Re-fetch data when the user focuses on the window. Great for fresh data!
			refetchOnWindowFocus: true,

			// If a request fails, retry it 2 more times before showing an error.
			retry: 2
		}
	}
})

function App() {
	return (
		// Provide the librarian to your entire application
		<QueryClientProvider client={queryClient}>
			<YourAppComponents />
		</QueryClientProvider>
	)
}

2. Key Hooks and Concepts to Master

  • useQuery: Your go-to for fetching data (GET requests).
  • queryKey: The unique ID for a query. It’s an array that can be simple ['todos'] or complex ['todos', { status: 'done', page: 2 }]. This is how React Query caches and tracks data.
  • useMutation: For any action that changes data (POST, PUT, DELETE). Its onSuccess callback is perfect for invalidating queries.
  • queryClient.invalidateQueries(): The most important function for mutations. After you add a new “todo,” you call queryClient.invalidateQueries({ queryKey: ['todos'] }). This tells React Query: “Hey, the ‘todos’ data is now out of date. Go and fetch it again automatically.”
  • Optimistic Updates: For an incredible UX, you can use useMutation to update the UI instantly, as if the server request already succeeded. If it fails, React Query will automatically roll back the change. The UI feels lightning-fast.

3. Special Tips for React Native

  • Network Management: Use the @react-native-community/netinfo library to tell React Query when the app is online or offline. This prevents failed requests when there’s no connection.
  • Focus Management: Instead of refetchOnWindowFocus, you can configure React Query to refetch data when a user navigates back to a specific screen, ensuring they always see the latest information.

Conclusion

By now, you should see that TanStack Query isn’t just a library—it’s a paradigm shift. It takes the messy, error-prone job of synchronizing your application’s state with a server and turns it into a declarative, robust, and almost effortless process.

By letting the “librarian” manage the state and using a professional “assistant” like axios to do the fetching, you can stop worrying about isLoading flags and focus on what truly matters: building a fantastic user experience.