DEV Community

Thanos Korakas
Thanos Korakas

Posted on

React template: Tanstack Query

Introduction

In my React template, I mentioned that Tanstack Query is one of my core libraries for handling HTTP requests and caching. In this post, I'll dive deeper into how I use it and why it's become an essential part of my development workflow.

Before Tanstack Query

Fetching data is a crucial part of any frontend application. I can't remember building a web app that didn't need to fetch and display data from the server.

I've tried GraphQL with Apollo and SWR, but nothing felt quite right until I discovered Tanstack Query. Before that, like most developers, I used React state (or Redux and other libraries) to handle server state.

Below is a simple example of fetching data using the fetch API and useEffect.

When the page renders for the first time, we fire a request, and when we get the data, we update the users state. React will then re-render to display the updated state.

const [users, setUsers] = useState([]);

useEffect(() => {
  fetch("https://jsonplaceholder.typicode.com/users")
    .then((res) => res.json())
    .then((data) => {
      setUsers(data);
    })
    .catch((err) => {
      console.error("Error fetching users:", err);
    });
}, []);
Enter fullscreen mode Exit fullscreen mode

This seems simple but lacks important features like loading and error handling.

Adding these features makes the code more complex:

const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);

useEffect(() => {
  fetch("https://jsonplaceholder.typicode.com/users")
      .then((res) => res.json())
      .then((data) => {
        setUsers(data);
      })
      .catch((err) => {
        console.error("Error fetching users:", err);
        setError(err);
      })
      .finally(() => {
        setLoading(false);
      })
}, []);
Enter fullscreen mode Exit fullscreen mode

We could improve this by using useReducer to manage state, but it doesn't solve the fundamental challenges.

I've also tried building custom hooks to reuse this logic, but new problems arise. What if we need user data in multiple components? We might end up making the same request multiple times. Moving the query higher in the component tree and using React Context helps, but creates other issues: what if the query depends on component state? The entire app re-renders every time we fetch data since the Provider is typically at the top level.

What about refetching users after creating a new one?

Tanstack Query

Tanstack Query solves many of these problems: loading states, error handling, pagination, infinite queries, dependent queries, polling, and more. But what really clicked for me was caching.

With Tanstack Query, you specify how long data remains valid. After that period, it automatically refetches. Even better, if two components request the same data, the library makes only one request and provides the result to both components.

This works through cache keys.

const { data: user, isLoading } = useQuery({
  queryKey: USER_QUERY_KEY,
  queryFn: getCurrentUser,
  gcTime: Infinity,
  staleTime: Infinity,
});
Enter fullscreen mode Exit fullscreen mode

In my React template, I use this approach for authentication. Instead of maintaining an isAuthenticated boolean in context or Zustand, I rely on caching. Each component that needs user data calls the useAuth hook, which makes an HTTP request to the /me endpoint. Since I cache indefinitely using staleTime: Infinity and gcTime: Infinity, it won't make another request until we invalidate the USER_QUERY_KEY cache.

When logging out, after calling the /logout endpoint, we clear the cache:

const clearUser = () => {
  queryClient.setQueryData(USER_QUERY_KEY, null);
  queryClient.removeQueries({ queryKey: USER_QUERY_KEY });
};
Enter fullscreen mode Exit fullscreen mode

The PrivateRoute component uses the useAuth hook, so it automatically re-renders and navigates the user to login.

Similarly, I listen for 401 responses using a Ky hook and clear the user cache, which redirects the user to login:

const beforeErrorHook: BeforeErrorHook = async error => {
  if (error.response?.status !== 401) {
    return error;
  }
  const requestUrl = error.request?.url || '';
  const isAuthMeEndpoint = requestUrl.includes('/auth/me');

  if (isAuthMeEndpoint) {
    return error;
  }

  queryClient.setQueryData(USER_QUERY_KEY, null);
  queryClient.removeQueries({ queryKey: USER_QUERY_KEY });
  return error;
};
Enter fullscreen mode Exit fullscreen mode

You can apply this pattern to any data that's used across the app but changes infrequently, like a list of countries for a dropdown. Cache it indefinitely, and it's available everywhere. When a user adds a new country, simply invalidate the cache after a successful response, and Tanstack Query will fetch the updated list including the newly created entry.

Using Tanstack Query helps separate server state from UI state, letting React focus on what it does best: managing the interface.

Tanstack Query is an incredibly powerful library with many more features. To learn more, I highly recommend this series by one of the core contributors.

Top comments (0)