React Query data fetching

DevOps Engineer
April 27, 2024
Updated on November 25, 2024
0 MIN READ
#hooks#graphql#javascript#typescript#react

Introduction

In modern web development, efficient data fetching and state management are critical for building responsive and scalable applications. React Query has emerged as a powerful library that simplifies data fetching, caching, synchronization, and updates in React applications. Unlike traditional state management solutions like Redux, React Query is specifically designed to handle server-state, making it an excellent choice for applications that rely heavily on APIs.

This blog post explores React Query’s core features, demonstrates how to implement data fetching, and highlights best practices for optimizing performance. Whether you're fetching data from REST APIs, GraphQL, or other sources, React Query provides a streamlined developer experience with minimal boilerplate.


What is React Query?

React Query is a data-fetching and state management library for React applications. It abstracts away the complexities of handling asynchronous data, such as caching, background updates, and error handling, while providing a declarative API. Key features include:

  • Automatic Caching: React Query caches fetched data by default, reducing redundant network requests.
  • Background Refetching: Data is automatically refreshed in the background when stale.
  • Optimistic Updates: Supports optimistic UI updates for mutations.
  • Pagination & Infinite Queries: Built-in support for paginated and infinite-scroll data.
  • Devtools: A dedicated DevTools panel for debugging queries and mutations.

Unlike client-state libraries (e.g., Redux), React Query focuses on server-state, meaning it handles data synchronization between your app and backend seamlessly.


Setting Up React Query

To get started with React Query, install the library using npm or yarn:

npm install @tanstack/react-query

Next, wrap your application with the QueryClientProvider to provide the React Query context:

import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; const queryClient = new QueryClient(); function App() { return ( <QueryClientProvider client={queryClient}> <YourAppComponent /> </QueryClientProvider> ); } export default App;

This setup enables React Query's features across your entire application.


Fetching Data with useQuery

The useQuery hook is the primary way to fetch data in React Query. It requires a query key (a unique identifier for the query) and a query function (an asynchronous function that fetches data).

Basic Example: Fetching a Todo List

import { useQuery } from '@tanstack/react-query'; const fetchTodos = async () => { const response = await fetch('https://jsonplaceholder.typicode.com/todos'); if (!response.ok) { throw new Error('Network response was not ok'); } return response.json(); }; function Todos() { const { data, isLoading, error } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos, }); if (isLoading) return <div>Loading...</div>; if (error) return <div>Error: {error.message}</div>; return ( <ul> {data?.map((todo) => ( <li key={todo.id}>{todo.title}</li> ))} </ul> ); }

Key Observations:

  • queryKey: Acts as a unique identifier for the query (used for caching and refetching).
  • queryFn: The function that fetches the data.
  • Status Handling: React Query provides isLoading, error, and data states out of the box.

Mutations with useMutation

While useQuery handles data fetching, useMutation is used for creating, updating, or deleting data (POST, PUT, DELETE requests).

Example: Adding a New Todo

import { useMutation, useQueryClient } from '@tanstack/react-query'; const addTodo = async (newTodo) => { const response = await fetch('https://jsonplaceholder.typicode.com/todos', { method: 'POST', body: JSON.stringify(newTodo), headers: { 'Content-Type': 'application/json' }, }); if (!response.ok) throw new Error('Failed to add todo'); return response.json(); }; function AddTodo() { const queryClient = useQueryClient(); const mutation = useMutation({ mutationFn: addTodo, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['todos'] }); // Refetch todos after mutation }, }); const handleSubmit = (e) => { e.preventDefault(); const formData = new FormData(e.target); const title = formData.get('title'); mutation.mutate({ title, completed: false }); }; return ( <form onSubmit={handleSubmit}> <input name="title" placeholder="Todo title" required /> <button type="submit" disabled={mutation.isPending}> {mutation.isPending ? 'Adding...' : 'Add Todo'} </button> </form> ); }

Key Features:

  • Optimistic Updates: You can implement optimistic UI updates using onMutate.
  • Automatic Refetching: invalidateQueries ensures the data stays fresh after mutations.
  • Pending State: isPending helps manage loading states during mutations.

Advanced Features and Best Practices

1. Pagination and Infinite Queries

React Query supports paginated data with useInfiniteQuery:

import { useInfiniteQuery } from '@tanstack/react-query'; const fetchPosts = async ({ pageParam = 1 }) => { const res = await fetch(`https://api.example.com/posts?page=${pageParam}`); return res.json(); }; function Posts() { const { data, fetchNextPage, hasNextPage } = useInfiniteQuery({ queryKey: ['posts'], queryFn: fetchPosts, getNextPageParam: (lastPage) => lastPage.nextPage, }); return ( <div> {data?.pages.map((page, i) => ( <div key={i}> {page.posts.map((post) => ( <div key={post.id}>{post.title}</div> ))} </div> ))} <button onClick={() => fetchNextPage()} disabled={!hasNextPage}> Load More </button> </div> ); }

2. Prefetching Data

Improve UX by prefetching data before it’s needed:

const queryClient = useQueryClient(); // Prefetch todos when hovering over a link const handleHover = () => { queryClient.prefetchQuery({ queryKey: ['todos'], queryFn: fetchTodos, }); };

3. Stale Time and Cache Control

Configure how long data remains fresh before refetching:

useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  staleTime: 5 * 60 * 1000, // Data stays fresh for 5 minutes
});

Conclusion

React Query revolutionizes data fetching in React applications by providing a robust, declarative API that handles caching, synchronization, and mutations effortlessly. By leveraging features like automatic background updates, pagination, and optimistic UI, developers can build performant apps with minimal boilerplate.

Key takeaways:

  • Use useQuery for fetching and caching data.
  • Handle mutations with useMutation and invalidate queries to keep data fresh.
  • Optimize performance with pagination, prefetching, and stale time configurations.

Whether you're building a small project or a large-scale application, React Query simplifies server-state management, allowing you to focus on delivering a great user experience. Give it a try in your next project!

Share this article