Skip to main content

React Query

Introduction

React Query is a powerful data-fetching and state management library for React applications that simplifies the process of fetching, caching, synchronizing, and updating server state. Traditional data fetching in React often becomes complex, involving multiple useEffect hooks, loading states, error handling, and manual caching. React Query solves these problems by providing a set of hooks that handle these complexities for you.

In this guide, we'll explore how React Query transforms data fetching in React applications, making it more efficient, predictable, and developer-friendly.

Why React Query?

Before diving into React Query, let's understand why it exists. Consider a typical data fetching scenario in React:

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

function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);

useEffect(() => {
const fetchUser = async () => {
setIsLoading(true);
try {
const response = await fetch(`https://api.example.com/users/${userId}`);
if (!response.ok) {
throw new Error('Failed to fetch user');
}
const data = await response.json();
setUser(data);
setError(null);
} catch (err) {
setError(err.message);
setUser(null);
} finally {
setIsLoading(false);
}
};

fetchUser();
}, [userId]);

if (isLoading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
if (!user) return <p>No user data</p>;

return (
<div>
<h2>{user.name}</h2>
<p>Email: {user.email}</p>
</div>
);
}

This approach works but has several limitations:

  • Managing loading and error states is verbose
  • No automatic re-fetching when data changes
  • No caching or background updates
  • No easy way to refresh data
  • No handling for stale data

React Query addresses all these challenges through a powerful API.

Getting Started with React Query

Installation

First, let's install React Query:

bash
npm install @tanstack/react-query
# or
yarn add @tanstack/react-query

Setting up the Query Client

To use React Query, you need to set up a QueryClient and wrap your application with a QueryClientProvider:

jsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';

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

const root = createRoot(document.getElementById('root'));

root.render(
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
);

Basic Queries with useQuery

The useQuery hook is the foundation of React Query. It takes a unique key and a function that returns a promise, and provides you with data, loading states, and more:

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

function UserProfile({ userId }) {
const fetchUser = async () => {
const response = await fetch(`https://api.example.com/users/${userId}`);
if (!response.ok) {
throw new Error('Failed to fetch user');
}
return response.json();
};

const { data, isLoading, error } = useQuery({
queryKey: ['user', userId],
queryFn: fetchUser
});

if (isLoading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
if (!data) return <p>No user data</p>;

return (
<div>
<h2>{data.name}</h2>
<p>Email: {data.email}</p>
</div>
);
}

Notice how much cleaner this code is compared to our previous example. React Query handles:

  • Loading states
  • Error states
  • Re-fetching when the query key changes (userId)
  • Caching the response

The Query Key

The queryKey is a unique identifier for your query. React Query uses this key to:

  1. Cache query results
  2. Determine when to refetch
  3. Share query results across components

Query keys can be simple strings, but are more commonly arrays allowing for dynamic values:

jsx
// Basic key
useQuery({ queryKey: ['todos'], queryFn: fetchTodos });

// Key with parameters
useQuery({ queryKey: ['todo', todoId], queryFn: () => fetchTodo(todoId) });

// Key with multiple parameters
useQuery({
queryKey: ['todos', { status, page }],
queryFn: () => fetchTodos(status, page)
});

Query Function

The query function should return a promise that resolves the data or throws an error. It can be any function that:

jsx
// Basic fetch
const fetchUsers = async () => {
const response = await fetch('https://api.example.com/users');
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
};

// With Axios
const fetchPosts = async () => {
const { data } = await axios.get('https://api.example.com/posts');
return data;
};

// With query function parameters
const fetchUserById = async ({ queryKey }) => {
// queryKey = ['user', userId]
const [_, userId] = queryKey;
const response = await fetch(`https://api.example.com/users/${userId}`);
if (!response.ok) {
throw new Error('Failed to fetch user');
}
return response.json();
};

Real-World Example: Building a Blog with React Query

Let's build a simple blog application that demonstrates React Query's capabilities:

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

// API functions
const fetchPosts = async () => {
const { data } = await axios.get('https://jsonplaceholder.typicode.com/posts');
return data;
};

const fetchUser = async (userId) => {
const { data } = await axios.get(`https://jsonplaceholder.typicode.com/users/${userId}`);
return data;
};

// Post component with user data
function Post({ post }) {
const { data: user, isLoading: isLoadingUser } = useQuery({
queryKey: ['user', post.userId],
queryFn: () => fetchUser(post.userId),
});

return (
<div className="post">
<h2>{post.title}</h2>
<p>{post.body}</p>
{isLoadingUser ? (
<p>Loading author...</p>
) : (
<div className="author">
<strong>Author:</strong> {user.name} ({user.email})
</div>
)}
</div>
);
}

// Blog component
function Blog() {
const { data: posts, isLoading, error, isError } = useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
});

if (isLoading) return <div>Loading posts...</div>;
if (isError) return <div>Error: {error.message}</div>;

return (
<div className="blog">
<h1>My Blog</h1>
{posts.slice(0, 10).map(post => (
<Post key={post.id} post={post} />
))}
</div>
);
}

export default Blog;

This example demonstrates several key features:

  1. Automatic caching of posts and users
  2. Parallel and nested queries (fetching users for each post)
  3. Deduplication of requests (React Query won't fetch the same user twice)

Advanced Features

Query Options

React Query provides numerous options to customize query behavior:

jsx
const { data } = useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
// Additional options:
refetchOnWindowFocus: true, // Default: true - Refetch when window regains focus
refetchOnMount: true, // Default: true - Refetch when component mounts
refetchOnReconnect: true, // Default: true - Refetch when reconnecting network
staleTime: 1000 * 60 * 5, // How long data remains fresh (5 minutes)
cacheTime: 1000 * 60 * 30, // How long unused data stays in cache (30 minutes)
retry: 3, // Number of retry attempts
retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000),
onSuccess: data => { // Callback when query succeeds
console.log('Data fetched successfully', data);
},
onError: error => { // Callback when query fails
console.error('Error fetching data', error);
},
select: data => { // Transform or select from the data
return data.map(item => ({
...item,
title: item.title.toUpperCase()
}));
}
});

Mutations with useMutation

For data modifications (POST, PUT, DELETE), React Query provides the useMutation hook:

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

function CreatePostForm() {
const queryClient = useQueryClient();
const [title, setTitle] = React.useState('');
const [body, setBody] = React.useState('');

const createPost = async (newPost) => {
const { data } = await axios.post('https://jsonplaceholder.typicode.com/posts', newPost);
return data;
};

const mutation = useMutation({
mutationFn: createPost,
onSuccess: (data) => {
// Invalidate and refetch posts query
queryClient.invalidateQueries({ queryKey: ['posts'] });
// Or update the cache directly
queryClient.setQueryData(['posts'], oldPosts => [data, ...oldPosts]);

// Clear form
setTitle('');
setBody('');
},
});

const handleSubmit = (e) => {
e.preventDefault();
mutation.mutate({ title, body, userId: 1 });
};

return (
<form onSubmit={handleSubmit}>
<h2>Create New Post</h2>
<div>
<label htmlFor="title">Title:</label>
<input
type="text"
id="title"
value={title}
onChange={(e) => setTitle(e.target.value)}
required
/>
</div>
<div>
<label htmlFor="body">Content:</label>
<textarea
id="body"
value={body}
onChange={(e) => setBody(e.target.value)}
required
/>
</div>
<button type="submit" disabled={mutation.isPending}>
{mutation.isPending ? 'Submitting...' : 'Submit Post'}
</button>
{mutation.isError && <p>Error: {mutation.error.message}</p>}
{mutation.isSuccess && <p>Post created successfully!</p>}
</form>
);
}

Query Invalidation

React Query allows you to invalidate queries to trigger refetches when data changes:

jsx
const queryClient = useQueryClient();

// Invalidate every query in the cache
queryClient.invalidateQueries();

// Invalidate all queries with a specific key
queryClient.invalidateQueries({ queryKey: ['posts'] });

// Invalidate a specific query
queryClient.invalidateQueries({ queryKey: ['post', postId] });

// Invalidate queries that start with 'post'
queryClient.invalidateQueries({ queryKey: ['post'] });

Pagination and Infinite Queries

React Query provides specialized hooks for pagination:

jsx
function PaginatedPosts() {
const [page, setPage] = React.useState(1);

const fetchPosts = async ({ queryKey }) => {
const [_, pageNum] = queryKey;
const res = await fetch(`https://api.example.com/posts?page=${pageNum}&limit=10`);
return res.json();
};

const { data, isLoading, isPreviousData } = useQuery({
queryKey: ['posts', page],
queryFn: fetchPosts,
keepPreviousData: true, // Keep previous page data while fetching next page
});

return (
<div>
{isLoading ? (
<p>Loading...</p>
) : (
<>
<ul>
{data.posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
<div>
<button
onClick={() => setPage(old => Math.max(old - 1, 1))}
disabled={page === 1}
>
Previous Page
</button>
<span> Page {page} </span>
<button
onClick={() => setPage(old => old + 1)}
disabled={!data.hasMore || isPreviousData}
>
Next Page
</button>
</div>
</>
)}
</div>
);
}

For infinite scrolling:

jsx
function InfinitePosts() {
const fetchPosts = async ({ pageParam = 1 }) => {
const res = await fetch(`https://api.example.com/posts?page=${pageParam}&limit=10`);
const data = await res.json();
return data;
};

const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
status,
} = useInfiniteQuery({
queryKey: ['infinitePosts'],
queryFn: fetchPosts,
getNextPageParam: (lastPage) => lastPage.nextPage || undefined,
});

return (
<div>
{status === 'loading' ? (
<p>Loading...</p>
) : status === 'error' ? (
<p>Error!</p>
) : (
<>
{data.pages.map((group, i) => (
<React.Fragment key={i}>
{group.posts.map(post => (
<div key={post.id}>{post.title}</div>
))}
</React.Fragment>
))}
<div>
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
>
{isFetchingNextPage
? 'Loading more...'
: hasNextPage
? 'Load More'
: 'Nothing more to load'}
</button>
</div>
</>
)}
</div>
);
}

Using React Query DevTools

React Query provides an excellent DevTools component for debugging queries:

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

function App() {
return (
<QueryClientProvider client={queryClient}>
{/* Your app components */}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}

The DevTools provide a visual interface to explore:

  • Active queries
  • Cached data
  • Query states
  • Refetch capabilities

Managing Query Client Options

You can customize the default behavior for all queries when you create your QueryClient:

jsx
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5 minutes
cacheTime: 1000 * 60 * 30, // 30 minutes
refetchOnWindowFocus: false,
retry: 1,
},
mutations: {
retry: 2,
},
},
});

Best Practices for React Query

  1. Use specific query keys: Make your query keys as specific as possible to avoid unintended cache invalidations.

  2. Normalize query keys: Establish conventions for your query keys (e.g., ['users', userId], ['posts', { page, status }]).

  3. Set appropriate stale times: Don't rely on default stale times. Think about how often your data changes and set appropriate stale times.

  4. Use placeholderData or initialData for a better user experience:

jsx
const { data } = useQuery({
queryKey: ['post', postId],
queryFn: () => fetchPost(postId),
placeholderData: previousPosts?.find(post => post.id === postId),
// OR
initialData: () => {
return queryClient.getQueryData(['posts'])?.find(post => post.id === postId);
},
});
  1. Prefetch data for an even better user experience:
jsx
// Prefetch a query
const prefetchPost = async (postId) => {
await queryClient.prefetchQuery({
queryKey: ['post', postId],
queryFn: () => fetchPost(postId),
});
};

// Use in navigation links
<button
onMouseEnter={() => prefetchPost(post.id)}
onClick={() => navigate(`/post/${post.id}`)}
>
View Post
</button>

Summary

React Query dramatically simplifies data fetching and state management in React applications by providing:

  • Automatic caching and background updates
  • Loading, error, and success states
  • Deduplication of identical requests
  • Query cancellation
  • Retry logic
  • Pagination and infinite query support
  • Easy mutation with cache updates
  • DevTools for debugging

By replacing complex data fetching code with React Query's declarative hooks, you can build more maintainable and performant React applications with less code.

Additional Resources

Here are some exercises to help you master React Query:

  1. Basic Query Exercise: Create a simple app that fetches and displays a list of todos using React Query.

  2. Query with Variables: Extend the todo app to fetch specific todos by ID.

  3. Mutation Exercise: Add functionality to create, update, and delete todos.

  4. Pagination Exercise: Implement paginated queries for a large dataset.

  5. Infinite Scroll: Transform the pagination into an infinite scroll implementation.

  6. Optimistic Updates: Implement optimistic updates when creating or updating data.

  7. Query Prefetching: Add query prefetching when hovering over items that lead to detailed views.

Happy coding!



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)