Why TanStack Query? A Guide to Caching and Invalidation | Chandrashekhar Kachawa | Tech Blog

Why TanStack Query? A Guide to Caching and Invalidation

react

If you’ve built any modern web application, you’ve dealt with the challenge of fetching, caching, and updating data from a server. It often involves a messy combination of useEffect, useState, loading spinners, and error states, which quickly becomes difficult to manage.

This is the problem TanStack Query (formerly known as React Query) was built to solve. It’s not just another data-fetching library; it’s a tool for managing server state.

Client State vs. Server State

First, we must understand the difference between two types of state:

  • Client State: State that lives entirely in the browser. Think: is a modal open? What is the current value of an input field? It’s synchronous and fully owned by the user.
  • Server State: State that lives on a server and is fetched asynchronously. It can be changed by other users, become outdated (“stale”), and requires caching to feel performant.

Managing server state with tools designed for client state (useState, useReducer) is like fitting a square peg in a round hole. TanStack Query provides the right tools for the right job.

Why Use TanStack Query? The Core Benefits

TanStack Query treats server state as a first-class citizen, giving you incredible features out-of-the-box:

  1. Aggressive Caching: It automatically caches data from your queries. If you request the same data again, it returns the cached version instantly before re-fetching in the background, making your application feel incredibly fast.

  2. Stale-While-Revalidate: This is the magic. It will show you cached (potentially stale) data right away, then automatically send a request in the background to see if anything has changed. If it has, the UI is seamlessly updated.

  3. Automatic Refetching: It can refetch data automatically when the user re-focuses the window or reconnects to the internet, ensuring the data on screen is always fresh.

  4. Simplified Logic: It drastically reduces the amount of code you need to write. No more complex useEffect hooks for data fetching. Your components simply ask for the data they need, and TanStack Query handles the rest.

How to Use It: A Practical Example

Let’s build a simple component that fetches and displays a list of todos, and then allows us to add a new one.

Step 1: Setup

First, wrap your application in a QueryClientProvider.

// In your main App.jsx or index.js
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

const queryClient = new QueryClient();

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      {/* The rest of your app */}
      <Todos />
    </QueryClientProvider>
  );
}

Step 2: Fetching Data with useQuery

The useQuery hook is for reading data. It needs two main things:

  • A unique query key to identify this query.
  • A fetcher function that returns a promise.
import { useQuery } from '@tanstack/react-query';

// An async function to fetch our data
const fetchTodos = async () => {
  const res = await fetch('https://api.example.com/todos');
  if (!res.ok) {
    throw new Error('Network response was not ok');
  }
  return res.json();
};

function Todos() {
  // useQuery handles fetching, caching, loading and error states for us
  const { data, error, isLoading, isFetching } = useQuery({
    queryKey: ['todos'], // A list-like key
    queryFn: fetchTodos,
  });

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>An error has occurred: {error.message}</div>;

  return (
    <div>
      <ul>
        {data.map(todo => (
          <li key={todo.id}>{todo.title}</li>
        ))}
      </ul>
      {isFetching ? 'Updating in background...' : ' '}
    </div>
  );
}

Notice isLoading vs isFetching. isLoading is only true for the very first fetch. isFetching is true anytime a background refetch is happening.

Step 3: Changing Data with useMutation and Invalidation

Now, how do we add a new todo and see it in our list? We use the useMutation hook for creating, updating, or deleting data.

The most important part is cache invalidation. After our mutation succeeds, we need to tell TanStack Query that our ['todos'] data is now stale. This will trigger an automatic refetch for any component using that query key.

import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';

// ... (keep fetchTodos and Todos component from above)

const addTodo = async (newTodo) => {
  const res = await fetch('https://api.example.com/todos', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(newTodo),
  });
  if (!res.ok) {
    throw new Error('Failed to create todo');
  }
  return res.json();
};

function AddTodoForm() {
  const queryClient = useQueryClient();

  const mutation = useMutation({
    mutationFn: addTodo,
    // When the mutation is successful...
    onSuccess: () => {
      // Invalidate the 'todos' query. This will cause it to refetch.
      console.log("Todo added! Invalidating cache...");
      queryClient.invalidateQueries({ queryKey: ['todos'] });
    },
  });

  const handleSubmit = (e) => {
    e.preventDefault();
    const title = e.target.elements.title.value;
    if (title) {
      mutation.mutate({ title, completed: false });
      e.target.reset();
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input type="text" name="title" placeholder="New Todo" />
      <button type="submit" disabled={mutation.isPending}>
        {mutation.isPending ? 'Adding...' : 'Add Todo'}
      </button>
      {mutation.isError && <div>{mutation.error.message}</div>}
    </form>
  );
}

// You would then render <AddTodoForm /> alongside your <Todos /> list.

When you submit the form, the following happens:

  1. useMutation sends the POST request.
  2. The server responds with success.
  3. The onSuccess callback fires.
  4. queryClient.invalidateQueries({ queryKey: ['todos'] }) tells TanStack Query that this key is stale.
  5. The Todos component, which uses useQuery with that same key, automatically refetches its data.
  6. The UI updates seamlessly with the new todo in the list.

This declarative approach is incredibly powerful. Your components don’t need to know why they are refetching, only that they need the data for ['todos']. The mutation component is responsible for triggering the invalidation, decoupling your logic beautifully.

Conclusion: Stop Fighting Server State

TanStack Query isn’t just a library; it’s a fundamental shift in how we handle data in modern applications. By drawing a clear line between client and server state, it frees developers from the repetitive and error-prone boilerplate of manual data fetching, caching, and synchronization.

The true power of TanStack Query lies in its declarative nature. Instead of telling your application how to fetch and update, you simply declare the data your components need. Through elegant mechanisms like automatic caching, stale-while-revalidate, and query invalidation, it handles the complex lifecycle of server state for you.

The result is cleaner, more maintainable code, a more responsive user experience, and fewer bugs. If you find yourself tangled in a web of useEffect hooks and loading flags, it’s time to let go. Embrace the paradigm shift and let TanStack Query manage your server state, so you can focus on building features, not fighting with data.

Latest Posts

Enjoyed this article? Follow me on X for more content and updates!

Follow @Ctrixdev