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:
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:
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
:
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:
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:
- Cache query results
- Determine when to refetch
- Share query results across components
Query keys can be simple strings, but are more commonly arrays allowing for dynamic values:
// 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:
// 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:
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:
- Automatic caching of posts and users
- Parallel and nested queries (fetching users for each post)
- 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:
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:
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:
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:
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:
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:
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
:
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
-
Use specific query keys: Make your query keys as specific as possible to avoid unintended cache invalidations.
-
Normalize query keys: Establish conventions for your query keys (e.g.,
['users', userId]
,['posts', { page, status }]
). -
Set appropriate stale times: Don't rely on default stale times. Think about how often your data changes and set appropriate stale times.
-
Use placeholderData or initialData for a better user experience:
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);
},
});
- Prefetch data for an even better user experience:
// 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:
-
Basic Query Exercise: Create a simple app that fetches and displays a list of todos using React Query.
-
Query with Variables: Extend the todo app to fetch specific todos by ID.
-
Mutation Exercise: Add functionality to create, update, and delete todos.
-
Pagination Exercise: Implement paginated queries for a large dataset.
-
Infinite Scroll: Transform the pagination into an infinite scroll implementation.
-
Optimistic Updates: Implement optimistic updates when creating or updating data.
-
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! :)