Managing Async State with TanStack Query

For years, Redux has been my go-to library for managing complex application state, especially when dealing with asynchronous operations. Its predictable state management and centralised store have been invaluable for most of my projects. But I have to admit Redux does have its cons, a bit of boilerplate (though this has been somewhat reduced with Redux Toolkit), slow data update speed, the need for thunks/sagas when dealing with async data and if we're being honest it's a bit too complex a tool to use when working with simple applications. Enter TanStack Query (formerly React Query), a powerful and elegant library specifically designed for managing, caching, synchronising, and updating server state in your web applications. When it comes to managing asynchronous data, it presents a strong substitute for Redux, requiring a lot less code and providing a more developer-friendly environment.   The Pain Points of Redux for Async Operations: Before diving into TanStack Query, let's go over why managing async state with Redux can be challenging and tricky: Significant Boilerplate: Fetching data typically involves defining action types, action creators (for request, success, and failure states), reducers to handle these actions, and often middleware like Redux Thunk or Redux Saga to orchestrate the asynchronous logic. This can lead to a lot of repetitive code.   Manual State Management: Developers are responsible for manually managing loading states, error states, and cached data within the Redux store. This requires careful implementation and can be prone to errors. Complex Data Synchronization: Ensuring data consistency across different components and handling background updates often requires intricate logic within reducers and middleware. Potential for Over-Engineering: For applications with primarily server-driven data, using the full power of Redux for every API call can feel like overkill. Using Tanstack Query for server side state management TanStack Query takes a different approach. It focuses specifically on simplifying the process of fetching, caching, and updating data from your backend. Here's how it shines as a Redux alternative for async state:   Declarative Data Fetching: Instead of manually dispatching actions and managing state transitions, TanStack Query allows you to declaratively define your data fetching logic using the useQuery hook. You provide a unique query key and a function that fetches your data, and TanStack Query handles the rest.   Automatic Caching and Deduping: TanStack Query intelligently caches fetched data in the background, preventing redundant API calls for the same data. It also automatically deduplicates concurrent requests for the same resource.   Background Updates and Refetching: The library provides mechanisms for automatic background updates based on various events (e.g., window focus, network reconnection) and offers easy ways to manually refetch data.   Optimistic Updates: TanStack Query facilitates optimistic updates, allowing you to immediately update the UI as if the mutation was successful, while handling potential errors in the background.   Simplified Error Handling: Error states are automatically managed and readily accessible within the useQuery result.  Mutations for Data Modification: For POST, PUT, DELETE, and other data modification operations, TanStack Query offers the useMutation hook, which simplifies handling these asynchronous actions and updating the cache accordingly. Working with TanStack Query a practical example First things first, create a query client and pass it into a query client provider that's wrapped around your app import { useQuery, QueryClient, QueryClientProvider, } from '@tanstack/react-query' import { getTodos, postTodo } from '../my-api' // Create a client const queryClient = new QueryClient() function App() { return ( // Provide the client to your App ) } Lets create a query to fetch a users profile data // queries/user const useGetUserProfile = () => { const GET_USER = async () => { return await axios.get("/user/profile"); }; const query = useQuery({ queryKey: ["getUserProfile"], queryFn: GET_USER, staleTime: Infinity, }); return { ...query, data: query.data?.data, }; }; Now we can import our query anywhere in our app to access our user data, for example // components/accountDetails import Spinner from "../loader-utils/spinner"; const AccountDetails = () => { const { data: user, isFetching } = useGetUserProfile(); if (isFetching) { return ; } return ( {user.name} {user.role} ); }; Here in our AccountDetails component we are accessing the successfully fetched user data and even the loading state directly from our query hook, without needing to set state ourselves. And by setting the staleTime field to Infinity, we are telling TanStack Query tha

Apr 10, 2025 - 20:58
 0
Managing Async State with TanStack Query

For years, Redux has been my go-to library for managing complex application state, especially when dealing with asynchronous operations. Its predictable state management and centralised store have been invaluable for most of my projects. But I have to admit Redux does have its cons, a bit of boilerplate (though this has been somewhat reduced with Redux Toolkit), slow data update speed, the need for thunks/sagas when dealing with async data and if we're being honest it's a bit too complex a tool to use when working with simple applications.
Enter TanStack Query (formerly React Query), a powerful and elegant library specifically designed for managing, caching, synchronising, and updating server state in your web applications. When it comes to managing asynchronous data, it presents a strong substitute for Redux, requiring a lot less code and providing a more developer-friendly environment.  

The Pain Points of Redux for Async Operations:

Before diving into TanStack Query, let's go over why managing async state with Redux can be challenging and tricky:

  • Significant Boilerplate: Fetching data typically involves defining action types, action creators (for request, success, and failure states), reducers to handle these actions, and often middleware like Redux Thunk or Redux Saga to orchestrate the asynchronous logic. This can lead to a lot of repetitive code.  
  • Manual State Management: Developers are responsible for manually managing loading states, error states, and cached data within the Redux store. This requires careful implementation and can be prone to errors.
  • Complex Data Synchronization: Ensuring data consistency across different components and handling background updates often requires intricate logic within reducers and middleware.
  • Potential for Over-Engineering: For applications with primarily server-driven data, using the full power of Redux for every API call can feel like overkill.

Using Tanstack Query for server side state management

TanStack Query takes a different approach. It focuses specifically on simplifying the process of fetching, caching, and updating data from your backend. Here's how it shines as a Redux alternative for async state:  

Declarative Data Fetching: Instead of manually dispatching actions and managing state transitions, TanStack Query allows you to declaratively define your data fetching logic using the useQuery hook. You provide a unique query key and a function that fetches your data, and TanStack Query handles the rest.  

  • Automatic Caching and Deduping: TanStack Query intelligently caches fetched data in the background, preventing redundant API calls for the same data. It also automatically deduplicates concurrent requests for the same resource.  
  • Background Updates and Refetching: The library provides mechanisms for automatic background updates based on various events (e.g., window focus, network reconnection) and offers easy ways to manually refetch data.  
  • Optimistic Updates: TanStack Query facilitates optimistic updates, allowing you to immediately update the UI as if the mutation was successful, while handling potential errors in the background.   Simplified Error Handling: Error states are automatically managed and readily accessible within the useQuery result. 
  • Mutations for Data Modification: For POST, PUT, DELETE, and other data modification operations, TanStack Query offers the useMutation hook, which simplifies handling these asynchronous actions and updating the cache accordingly.

Working with TanStack Query a practical example

First things first, create a query client and pass it into a query client provider that's wrapped around your app

import {
  useQuery,
  QueryClient,
  QueryClientProvider,
} from '@tanstack/react-query'
import { getTodos, postTodo } from '../my-api'

// Create a client
const queryClient = new QueryClient()

function App() {
  return (
    // Provide the client to your App
    
      
) }

Lets create a query to fetch a users profile data

// queries/user

const useGetUserProfile = () => {
  const GET_USER = async () => {
    return await axios.get("/user/profile");
  };

  const query = useQuery({
    queryKey: ["getUserProfile"],
    queryFn: GET_USER,
    staleTime: Infinity,
  });

  return {
    ...query,
    data: query.data?.data,
  };
};


Now we can import our query anywhere in our app to access our user data, for example

// components/accountDetails

import Spinner from "../loader-utils/spinner";

const AccountDetails = () => {
  const { data: user, isFetching } = useGetUserProfile();
  if (isFetching) {
    return ;
  }

  return (
    

{user.name}

{user.role}

); };

Here in our AccountDetails component we are accessing the successfully fetched user data and even the loading state directly from our query hook, without needing to set state ourselves. And by setting the staleTime field to Infinity, we are telling TanStack Query that this query can be cached indefinitely, so we don't have to worry about our query refetching each time we call it in any component, although we can still set to behave like that if needed. If there are any updates to the data on the backend, all we need to do is to invalidate our user profile query cache and refetch the data, here is a quick example

// components/updateUser

const UpdateUser = ({ userName }) => {
  const [name, setName] = useState(userName);
  // Access the client
  const queryClient = useQueryClient();
  // update user mutation
  const { mutateAsync } = useMutation({
    mutationFn: async (data) => {
      return await axios.post("/user", data);
    },
  });
  const handleUpdateName = async () => {
    try {
      await mutateAsync({
        userName: name,
      });
// manually invalidate the cached getUserProfile query
      await queryClient.invalidateQueries({
        queryKey: ["getUserProfile"],
      });
    } catch {}
  };

  return (
    
setName(e.target.value)} />
); };

After making our mutation, we immediately invalidate our getUserProfile query cache; this tells TanStack Query that we want this query to be refetched as the data is now stale.

As you can see this is way less code and complexity than if we used Redux, TanStack Query handles the loading and error states, caching, and even background refetching automatically.

TanStack Query and Redux can still coexist peacefully in the same application. You might use TanStack Query for managing server state and Redux for managing global client-side UI state or other application-specific data.