Revolutionizing Data Management with TanStack React Query (Chapter 1)

venkatesh prasadvenkatesh prasad
10 min read

Introduction

In the fast-paced world of web development, staying ahead of the curve is paramount. As React developers, we are always on the lookout for tools and libraries that simplify complex tasks and enhance the user experience. Enter TanStack React Query, a powerful state management library that has taken the React community by storm. In this blog post, we will embark on a journey to explore the innovative features and benefits of TanStack React Query, showcasing how it's reshaping the way we handle data in modern web applications.

What is TanStack React Query?

At its core, React Query is a library designed to simplify data fetching, caching, synchronization, and state management in React applications. Developed by Tanner Linsley and his team at TanStack, React Query has gained immense popularity for its simplicity, flexibility, and robust features. It provides an elegant solution to the challenges developers face when dealing with asynchronous data and complex UI interactions.

Key Features of React Query

  1. Effortless Data Fetching: React Query v3 streamlines the process of data fetching from various sources, such as REST APIs, GraphQL endpoints, and more. Its intuitive API makes asynchronous data retrieval a breeze, reducing the boilerplate code typically associated with network requests.

  2. Automatic Caching and Query Invalidation: React Query v3 intelligently caches fetched data, ensuring that your application remains responsive and efficient. It handles query invalidation and cache management seamlessly, simplifying the task of keeping data up-to-date.

  3. Optimistic Updates: With React Query v3, you can implement optimistic updates, allowing your application to instantly reflect user interactions while awaiting confirmation from the server. This creates a smoother and more responsive user experience.

  4. Real-time Data Synchronization: React Query v3 seamlessly integrates with real-time data sources, enabling automatic updates when the server data changes. Whether it's chat applications, collaborative tools, or live dashboards, React Query ensures your app remains in sync with the latest data.

  5. Pagination and Infinite Loading: Handling large datasets is effortless with React Query v3. It supports pagination and infinite loading out of the box, enhancing the performance and usability of applications dealing with extensive data.

Let's walk through a simple example of using React Query v3 in a React application to fetch and display data from an API endpoint. In this example, we'll create a list of users fetched from a JSONPlaceholder API.

Step 1: Setting Up the Project

First, you need to set up a new React project if you haven't already. You can use Create React App or any other method you prefer. Make sure to install React Query as a dependency:

npm install react-query

Step 2: Fetching Data Using React Query

Let's compare traditional data fetching methods with React Query to understand the difference in approach and simplicity. We will use the example of fetching a list of users from an API endpoint.

Traditional Data Fetching (Before React Query):

Before the introduction of React Query, data fetching in React typically involved using fetch or axios inside useEffect to make asynchronous calls to APIs. Here's an example of how you might fetch users without React Query:

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

const UserList = () => {
  const [users, setUsers] = useState([]);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await fetch('https://jsonplaceholder.typicode.com/users');
        const data = await response.json();
        setUsers(data);
        setIsLoading(false);
      } catch (error) {
        console.error('Error fetching data:', error);
        setIsLoading(false);
      }
    };

    fetchData();
  }, []); // Empty dependency array ensures the effect runs only once after the initial render

  if (isLoading) {
    return <div>Loading...</div>;
  }

  return (
    <div>
      <h1>User List</h1>
      <ul>
        {users.map((user) => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
    </div>
  );
};

export default UserList;

In this approach, you manually handle loading states, error handling, and data updating. While it works, it involves writing more boilerplate code and managing state transitions yourself.

Data Fetching Using React Query:

Now, let's see how the same data fetching operation can be performed using React Query, demonstrating the simplicity it brings to the process:

import React from 'react';
import { useQuery } from 'react-query';

const fetchUsers = async () => {
  const response = await fetch('https://jsonplaceholder.typicode.com/users');
  const data = await response.json();
  return data;
};

const UserList = () => {
  const { data: users, isLoading, isError } = useQuery('users', fetchUsers);

  if (isLoading) {
    return <div>Loading...</div>;
  }

  if (isError) {
    return <div>Error fetching data</div>;
  }

  return (
    <div>
      <h1>User List</h1>
      <ul>
        {users.map((user) => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
    </div>
  );
};

export default UserList;

In this React Query example, the useQuery hook handles data fetching, loading states, and error handling in a concise manner. It abstracts away the complexities of asynchronous operations, providing a clean and intuitive API. By using React Query, you eliminate the need for manual state management, making your code more readable, maintainable, and efficient.

Comparatively, React Query simplifies the process, reduces boilerplate code, and improves the developer experience, making it a preferred choice for data fetching in modern React applications.

Let's extend the previous example to demonstrate stale time concepts in React Query.

import React from 'react';
import { useQuery } from 'react-query';

const fetchUsers = async () => {
  const response = await fetch('https://jsonplaceholder.typicode.com/users');
  const data = await response.json();
  return data;
};

const UserList = () => {
  const { data: users, isLoading, isError } = useQuery('users', fetchUsers, {
    staleTime: 60000, // Data will be considered stale after 1 minute (60,000 milliseconds)
  });

  if (isLoading) {
    return <div>Loading...</div>;
  }

  if (isError) {
    return <div>Error fetching data</div>;
  }

  return (
    <div>
      <h1>User List</h1>
      <ul>
        {users.map((user) => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
    </div>
  );
};

export default UserList;

In the provided example, the staleTime option is set to 60000 (1 minute), which means the data will be considered stale after 1 minute. Here's how staleTime works in the context of the user list example:

  1. Initial Fetch:

    • When the UserList component is first rendered, React Query triggers a fetch operation to get the list of users from the API endpoint.

    • The fetched data is stored in the cache and displayed in the component.

  2. Data is Cached:

    • The fetched user data is now stored in the cache.

    • As long as the component is re-rendered within the next 1 minute (60,000 milliseconds), React Query will use the cached data to render the component without making another API request.

  3. Data Becomes Stale:

    • After 1 minute (as per the staleTime setting), the data is considered stale. This means React Query will still display the cached data to the user.

    • If the UserList component is re-rendered within this 1-minute window, the stale data will be shown, ensuring a smooth user experience without waiting for a fresh fetch from the server.

  4. Background Refetch:

    • If the UserList component is re-rendered after the 1-minute stale time has passed, React Query will automatically trigger a background refetch.

    • While React Query serves the stale data to the user, it initiates a new fetch operation in the background to get the most recent user data.

    • Once the new data is fetched, it will replace the stale data in the component, ensuring that the displayed information is always up-to-date without causing a delay in rendering.

By utilizing staleTime, React Query strikes a balance between displaying cached data for a responsive user interface and ensuring data freshness by automatically fetching new data in the background when necessary. This feature significantly enhances the user experience by providing real-time data updates without compromising performance.

Now let's see how to use useQuery when we have reusable component which takes an id and have to fetch data if ID is valid and changes.

  1. Manual useEffect Approach:

In this manual approach, you have an id variable, and you want to fetch products whenever the id is truthy (non-null and non-undefined).

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

const ProductDetails = ({ id }) => {
  useEffect(() => {
    if (id) {
      fetchProducts(id);
    }
  }, [id]);

  // Rest of the component logic...
};
  1. React Query Approach:

With React Query, you can achieve the same functionality in a more streamlined manner using the useQuery hook with the enabled option

import React from 'react';
import { useQuery } from 'react-query';

const fetchProduct = async (id) => {
  const response = await fetch(`https://api.example.com/products/${id}`);
  const data = await response.json();
  return data;
};

const ProductDetails = ({ id }) => {
  const { data: product, isLoading, isError } = useQuery(['product', id], () => fetchProduct(id), {
    enabled: !!id, // Fetch data only if id is truthy (non-null and non-undefined)
  });

  // Rest of the component logic...
};

In this React Query approach, the useQuery hook takes the id variable as part of the query key and uses the enabled option. The enabled option ensures that the query is executed only when id is truthy. If id is falsy (null or undefined), the query will not be executed, preventing unnecessary network requests.

By using React Query in this manner, you simplify the logic, eliminate the need for a manual useEffect, and ensure efficient data fetching based on the id variable. React Query handles the caching, stale time, and data synchronization automatically, providing a cleaner and more declarative solution.

Let's explore a few more concepts of React Query with examples.

1. Query Invalidation and Refetching:

In React Query, you can manually trigger query invalidation and refetching. For instance, consider a scenario where you want to refetch the user data when a button is clicked.

import React from 'react';
import { useQuery, useMutation, QueryClient, QueryClientProvider } from 'react-query';

const queryClient = new QueryClient();

const fetchUsers = async () => {
  const response = await fetch('https://jsonplaceholder.typicode.com/users');
  const data = await response.json();
  return data;
};

const deleteUser = async (id) => {
  await fetch(`https://jsonplaceholder.typicode.com/users/${id}`, {
    method: 'DELETE',
  });
};

const UserList = () => {
  const { data: users, isLoading, isError, refetch } = useQuery('users', fetchUsers);

  const handleDelete = async (id) => {
    await deleteUser(id);
    // Manually trigger query invalidation and refetching
    refetch();
  };

  if (isLoading) {
    return <div>Loading...</div>;
  }

  if (isError) {
    return <div>Error fetching data</div>;
  }

  return (
    <div>
      <h1>User List</h1>
      <ul>
        {users.map((user) => (
          <li key={user.id}>
            {user.name} <button onClick={() => handleDelete(user.id)}>Delete</button>
          </li>
        ))}
      </ul>
    </div>
  );
};

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <div className="App">
        <UserList />
      </div>
    </QueryClientProvider>
  );
}

export default App;

In this example, the refetch function is used to manually refetch the user data when the "Delete" button is clicked. This ensures that the user list stays up-to-date after a deletion operation.

2. Mutations with React Query:

React Query provides a useMutation hook for handling data mutations (create, update, delete operations) with ease.

import React from 'react';
import { useMutation, useQueryClient, QueryClient, QueryClientProvider } from 'react-query';

const queryClient = new QueryClient();

const createUser = async (newUser) => {
  const response = await fetch('https://jsonplaceholder.typicode.com/users', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(newUser),
  });
  const data = await response.json();
  return data;
};

const CreateUserForm = () => {
  const queryClient = useQueryClient();
  const mutation = useMutation(createUser, {
    onSuccess: () => {
      // Invalidate and refetch the user list after a new user is created
      queryClient.invalidateQueries('users');
    },
  });

  const handleSubmit = async (event) => {
    event.preventDefault();
    const formData = new FormData(event.target);
    const newUser = {
      name: formData.get('name'),
      email: formData.get('email'),
    };

    mutation.mutate(newUser);
  };

  return (
    <form onSubmit={handleSubmit}>
      <label>Name:</label>
      <input type="text" name="name" required />
      <label>Email:</label>
      <input type="email" name="email" required />
      <button type="submit" disabled={mutation.isLoading}>
        {mutation.isLoading ? 'Creating...' : 'Create User'}
      </button>
    </form>
  );
};

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <div className="App">
        <CreateUserForm />
      </div>
    </QueryClientProvider>
  );
}

export default App;

In this example, the useMutation hook is used to handle the creation of a new user. The onSuccess callback is used to invalidate and refetch the user list after a new user is successfully created. This ensures that the user list stays updated with the latest data.

That's it for part one.

These examples showcase the flexibility and power of React Query in handling various data-fetching and state management scenarios. React Query's intuitive API and seamless integration with React applications make it a valuable tool for building modern and efficient web applications.

In this journey through the fundamental concepts of React Query, we've explored the power and simplicity it brings to data fetching, caching, and state management in React applications. We've delved into the basics of fetching data, understanding caching strategies, and handling scenarios where data might be stale. These concepts serve as the foundation of your React Query knowledge, enabling you to build responsive and efficient user interfaces with ease.

As we wrap up this first part of our exploration, remember that these are just the building blocks. React Query offers a wealth of advanced features and techniques that can further elevate your application development experience. In the upcoming Part 2 of this blog series, we will dive deep into advanced topics such as query variables, pagination, optimistic updates, and more. We'll explore how React Query seamlessly integrates with real-time data sources and how it empowers you to tackle complex data scenarios.

So, stay tuned for the next chapter, where we'll unlock the full potential of React Query and take our understanding to the next level. Happy coding!

0
Subscribe to my newsletter

Read articles from venkatesh prasad directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

venkatesh prasad
venkatesh prasad