Samuel Fajreldines

I am a specialist in the entire JavaScript and TypeScript ecosystem.

I am expert in AI and in creating AI integrated solutions.

I am expert in DevOps and Serverless Architecture

I am expert in PHP and its frameworks.

+55 (51) 99226-5039 samuelfajreldines@gmail.com

The Ultimate Guide to Using React Query: Why and How to Implement It

Introduction

In the modern web development landscape, efficient data fetching and state management are crucial for building responsive and user-friendly applications. React Query, often described as "the missing data-fetching library for React," simplifies these aspects by providing powerful hooks for fetching, caching, and updating asynchronous data in React applications.

In this comprehensive guide, we'll explore why React Query has become an indispensable tool for developers, how to integrate it into your projects, and walk through a complete use case to solidify your understanding.

What is React Query?

React Query is a library for fetching, caching, and updating data in React applications without the need for global state management solutions like Redux. It leverages React's hooks system to provide an intuitive and declarative API for handling server state.

By abstracting away the complexities of data fetching, synchronization, and caching, React Query allows developers to focus on building features rather than boilerplate code. It handles under-the-hood tasks such as caching, background updates, and stale data management, providing a smoother and more efficient user experience.

Why Use React Query?

Improved Data Fetching

Fetching data from APIs is a common requirement in web applications. Traditionally, developers handled this using useEffect, managing loading states, errors, and updates manually. React Query automates these aspects, reducing code complexity and potential bugs.

Automatic Caching and Updating

React Query caches fetched data and intelligently updates it when necessary. This means that if multiple components request the same data, React Query serves it from the cache, reducing unnecessary network requests. It also keeps the data fresh by refetching it in the background when it becomes stale.

Simplifies State Management

By handling server state separately from client state, React Query eliminates the need for complex state management libraries for many applications. It provides a straightforward API for managing asynchronous data, which can significantly simplify your codebase.

Better User Experience

With features like background refetching, pagination, and optimistic updates, React Query enhances the user experience by making applications faster and more responsive.

Getting Started with React Query

Installation

To start using React Query in your project, you need to install it along with its required peer dependency:

npm install @tanstack/react-query

Basic Usage

Before using React Query hooks, you need to set up a QueryClient and wrap your application with a QueryClientProvider:

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

const queryClient = new QueryClient();

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      {/* ... your app components */}
    </QueryClientProvider>
  );
}

export default App;

Understanding Queries and Mutations

React Query provides two primary hooks: useQuery for fetching data (queries) and useMutation for updating data (mutations).

useQuery Hook

The useQuery hook is used to fetch data asynchronously:

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

function Todos() {
  const { isLoading, error, data } = useQuery(['todos'], fetchTodos);

  if (isLoading) return 'Loading...';

  if (error) return 'An error occurred';

  return (
    <ul>
      {data.map(todo => (
        <li key={todo.id}>{todo.title}</li>
      ))}
    </ul>
  );
}

useMutation Hook

The useMutation hook is used for creating, updating, or deleting data:

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

function AddTodo() {
  const queryClient = useQueryClient();
  const mutation = useMutation(addTodo, {
    onSuccess: () => {
      // Invalidate and refetch
      queryClient.invalidateQueries(['todos']);
    },
  });

  return (
    <button
      onClick={() => {
        mutation.mutate({ title: 'New Todo' });
      }}
    >
      Add Todo
    </button>
  );
}

Setting Up a Complete Use Case

Let's build a simple application to demonstrate how to use React Query in a real-world scenario. We'll create a basic CRUD (Create, Read, Update, Delete) app for managing a list of users.

Overview of the Application

Our application will have the following features:

  • Display a list of users
  • Add a new user
  • Update an existing user
  • Delete a user

Setting Up the Project

First, create a new React project using Create React App:

npx create-react-app react-query-demo
cd react-query-demo
npm install @tanstack/react-query axios

We'll use Axios for making HTTP requests.

Creating the API

For simplicity, we'll use the JSONPlaceholder API, which provides fake online REST APIs for testing.

Implementing React Query in the Application

  1. Setup QueryClient
// index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import App from './App';

const queryClient = new QueryClient();

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <QueryClientProvider client={queryClient}>
    <App />
  </QueryClientProvider>
);
  1. Fetch Users
// App.js
import React from 'react';
import axios from 'axios';
import { useQuery } from '@tanstack/react-query';

function App() {
  const { isLoading, error, data } = useQuery(['users'], () =>
    axios.get('https://jsonplaceholder.typicode.com/users').then(res => res.data)
  );

  if (isLoading) return <p>Loading users...</p>;

  if (error) return <p>An error occurred: {error.message}</p>;

  return (
    <div>
      <h1>User List</h1>
      <ul>
        {data.map(user => (
          <li key={user.id}>
            {user.name}
            {/* Update and Delete buttons will go here */}
          </li>
        ))}
      </ul>
      {/* Components for adding a user will go here */}
    </div>
  );
}

export default App;
  1. Add a User

Since JSONPlaceholder doesn't actually change data, we'll simulate adding a user.

// AddUser.js
import React, { useState } from 'react';
import axios from 'axios';
import { useMutation, useQueryClient } from '@tanstack/react-query';

function AddUser() {
  const queryClient = useQueryClient();
  const [name, setName] = useState('');

  const addUserMutation = useMutation(
    newUser => axios.post('https://jsonplaceholder.typicode.com/users', newUser).then(res => res.data),
    {
      onSuccess: data => {
        // Update the users list
        queryClient.setQueryData(['users'], oldData => [...oldData, data]);
      },
    }
  );

  const handleSubmit = e => {
    e.preventDefault();
    addUserMutation.mutate({ name });
    setName('');
  };

  return (
    <form onSubmit={handleSubmit}>
      <input value={name} onChange={e => setName(e.target.value)} placeholder="Name" required />
      <button type="submit">Add User</button>
    </form>
  );
}

export default AddUser;

Include <AddUser /> in your App component.

// App.js
// ...
import AddUser from './AddUser';
// ...
function App() {
  // existing code
  return (
    <div>
      {/* existing code */}
      <AddUser />
    </div>
  );
}
// ...
  1. Update a User
// UpdateUser.js
import React, { useState } from 'react';
import axios from 'axios';
import { useMutation, useQueryClient } from '@tanstack/react-query';

function UpdateUser({ user }) {
  const queryClient = useQueryClient();
  const [name, setName] = useState(user.name);

  const updateUserMutation = useMutation(
    updatedUser => axios.put(`https://jsonplaceholder.typicode.com/users/${user.id}`, updatedUser).then(res => res.data),
    {
      onSuccess: data => {
        queryClient.setQueryData(['users'], oldData =>
          oldData.map(u => (u.id === user.id ? data : u))
        );
      },
    }
  );

  const handleSubmit = e => {
    e.preventDefault();
    updateUserMutation.mutate({ name });
  };

  return (
    <form onSubmit={handleSubmit}>
      <input value={name} onChange={e => setName(e.target.value)} required />
      <button type="submit">Update</button>
    </form>
  );
}

export default UpdateUser;

Include <UpdateUser user={user} /> in your user list rendering.

// App.js
// ...
<li key={user.id}>
  {user.name}
  <UpdateUser user={user} />
  {/* Delete button */}
</li>
// ...
  1. Delete a User
// DeleteUser.js
import React from 'react';
import axios from 'axios';
import { useMutation, useQueryClient } from '@tanstack/react-query';

function DeleteUser({ userId }) {
  const queryClient = useQueryClient();

  const deleteUserMutation = useMutation(
    () => axios.delete(`https://jsonplaceholder.typicode.com/users/${userId}`),
    {
      onSuccess: () => {
        queryClient.setQueryData(['users'], oldData =>
          oldData.filter(user => user.id !== userId)
        );
      },
    }
  );

  return <button onClick={() => deleteUserMutation.mutate()}>Delete</button>;
}

export default DeleteUser;

Include <DeleteUser userId={user.id} /> in your user list rendering.

// App.js
// ...
<li key={user.id}>
  {user.name}
  <UpdateUser user={user} />
  <DeleteUser userId={user.id} />
</li>
// ...

Advanced Features

Handling Query States

React Query provides various states to manage loading, error, and success scenarios effectively.

const {
  isLoading,
  isError,
  data,
  error,
  isFetching,
  refetch,
} = useQuery(['todos'], fetchTodos);
  • isLoading: Indicates if the query is currently loading.
  • isError: Becomes true if the query encounters an error.
  • data: The data fetched by the query.
  • error: The error object if the query fails.
  • isFetching: Remains true during background refetches.
  • refetch: Function to manually refetch data.

Caching Strategies

Adjusting cache settings can optimize performance:

useQuery(['users'], fetchUsers, {
  cacheTime: 1000 * 60 * 10, // Cache data for 10 minutes
  staleTime: 1000 * 60 * 5,  // Data is fresh for 5 minutes
});

Polling or Interval Fetching

Keep data updated at regular intervals using refetchInterval:

useQuery(['notifications'], fetchNotifications, {
  refetchInterval: 1000 * 60, // Refetch every 60 seconds
});

Retrying Failed Queries

Automatically retry failed queries:

useQuery(['users'], fetchUsers, {
  retry: 3, // Retry up to 3 times
  retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000),
});

React Query Devtools

For debugging and inspecting queries:

npm install @tanstack/react-query-devtools
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';

function App() {
  return (
    // ...
    <ReactQueryDevtools initialIsOpen={false} />
  );
}

Server-Side Rendering

React Query supports SSR with Next.js:

// pages/_app.js
// Implement getServerSideProps or getStaticProps

Custom Query Functions

Standardize API calls with custom fetchers:

const fetcher = url => axios.get(url).then(res => res.data);

useQuery(['users'], () => fetcher('/api/users'));

Transforming Data

Use the select option to modify data:

useQuery(['users'], fetchUsers, {
  select: data => data.map(user => user.name),
});

Best Practices

  • Key Management: Use descriptive query keys for clarity.
  • Error Handling: Utilize onError and onSuccess callbacks.
  • Avoid Over-Fetching: Implement pagination and infinite scrolling.
  • Performance Optimization: Adjust staleTime and cacheTime appropriately.

Comparison with Other Libraries

Compared to Redux or MobX, React Query focuses on server state. For applications heavily reliant on server data, React Query can simplify the codebase by handling data fetching and caching effectively.

Alternatives to React Query

  • SWR: A React Hooks library for remote data fetching developed by Vercel.
  • Apollo Client: A comprehensive state management library for GraphQL.

Conclusion

React Query simplifies data fetching and state management in React applications, leading to cleaner code and a better user experience. By handling caching, background updates, and synchronization, it allows developers to focus on building features rather than the intricacies of asynchronous data handling.

In this guide, we've explored the reasons to use React Query, how to integrate it into your application, and walked through a complete use case. Whether you're building a small app or a large-scale application, React Query can significantly enhance your development workflow.

Further Reading



Resume

Experience

  • SecurityScoreCard

    Nov. 2023 - Present

    New York, United States

    Senior Software Engineer

    I joined SecurityScorecard, a leading organization with over 400 employees, as a Senior Full Stack Software Engineer. My role spans across developing new systems, maintaining and refactoring legacy solutions, and ensuring they meet the company's high standards of performance, scalability, and reliability.

    I work across the entire stack, contributing to both frontend and backend development while also collaborating directly on infrastructure-related tasks, leveraging cloud computing technologies to optimize and scale our systems. This broad scope of responsibilities allows me to ensure seamless integration between user-facing applications and underlying systems architecture.

    Additionally, I collaborate closely with diverse teams across the organization, aligning technical implementation with strategic business objectives. Through my work, I aim to deliver innovative and robust solutions that enhance SecurityScorecard's offerings and support its mission to provide world-class cybersecurity insights.

    Technologies Used:

    Node.js Terraform React Typescript AWS Playwright and Cypress