In this guide, we’ll explore how to use React Query (Tanstack React Query), its core features, and best practices to optimize your React or Next.js applications.
What is React Query (Tanstack React Query)?
React Query (now known as TanStack Query or Tanstack React Query) is a powerful data-fetching and state management library that simplifies API requests, caching, background updates, and state synchronization—all with minimal boilerplate.
Unlike other state management libraries like Redux, which require manually handling API states, React Query automates the process, making data fetching efficient, scalable, and hassle-free. It works seamlessly with REST and GraphQL APIs, improving both performance and developer experience.
With React Query, you get:
- Automatic caching & refetching: Keeps data fresh without extra effort.
- Background updates: Fetches new data without blocking UI.
- Pagination & Infinite Scrolling: Built-in support for large datasets.
- Optimistic Updates: Improves UX by instantly updating UI before API confirmation.
- Works with Next.js: Supports server-side rendering (SSR) and prefetching for SEO benefits. Learn More About SSR in Next.js.
React Query makes managing server-state in React easy, which is why it's a popular choice for modern web apps.
Why Use React Query?
Managing server-state in React apps can be complex, especially when dealing with loading states, caching, background updates, and error handling. React Query makes it easy by providing a simple way to fetch, cache, and sync data with minimal effort.
Getting Started with React Query (Tanstack React Query)
Installation
First, install React Query or tanstack React Query and React Query DevTools:
sh
1npm install @tanstack/react-query @tanstack/react-query-devtools
Setup QueryClientProvider
Wrap your application with QueryClientProvider
to enable React Query globally.
For React (App.jsx or main.jx file):
.jsx
1import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
2
3const queryClient = new QueryClient();
4
5function App() {
6return (
7 <QueryClientProvider client={queryClient}>
8 <YourComponent />
9 </QueryClientProvider>
10);
11}
12
13export default App;
14
For Next.js (layout.tsx):
For setting React Query in NextJs Create separate component ReactQueryClientProvider
:
.tsx
1'use client';
2
3import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
4import { useState } from 'react';
5
6const ReactQueryClientProvider = ({ children }: { children: React.ReactNode }) => {
7const [queryClient] = useState(() => new QueryClient());
8return (
9 <QueryClientProvider client={queryClient}>
10 {children}
11 </QueryClientProvider>
12);
13};
14
15export default ReactQueryClientProvider;
16
and then import it in main layour.tsx
.tsx
1import ReactQueryClientProvider from './components/ReactQueryClientProvider';
2
3export default function RootLayout({ children }: { children: React.ReactNode }) {
4return (
5 <ReactQueryClientProvider>
6 <html lang="en">
7 <body>{children}</body>
8 </html>
9 </ReactQueryClientProvider>
10);
11}
12
Enable React Query DevTools (Optional)
Use React Query DevTools for debugging queries in your app. You can enable it by adding:
.jsx
1import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
2
3<QueryClientProvider client={queryClient}>
4<YourComponent />
5<ReactQueryDevtools initialIsOpen={false} />
6</QueryClientProvider>;
7
Fetch Data in React Query useQuery
One of the biggest advantages of React Query useQuery is how easily it manages loading, error, and success states without manually handling them. When fetching data with useQuery
, you get built-in states that can be used directly in your UI.
Basic Example: Handling States
.jsx
1import { useQuery } from "@tanstack/react-query";
2import axios from "axios";
3
4// Create fetcher function
5const fetchUsers = async () => {
6const { data } = await axios.get("https://jsonplaceholder.typicode.com/users");
7return data;
8};
9
10export default function UsersList() {
11const { data, isLoading, isError, error, isSuccess } = useQuery({
12 queryKey: ["users"],
13 queryFn: fetchUsers, // pass fetcher function here
14});
15
16if (isLoading) return <p>Loading users...</p>;
17if (isError) return <p>Error: {error.message}</p>;
18
19return (
20 <ul>
21 {data.map((user) => (
22 <li key={user.id}>{user.name}</li>
23 ))}
24 </ul>
25);
26}
Breakdown of States in React Query useQuery
- isLoading: When the API call is in progress.
- isError: If the request fails, this returns true along with the error object.
- isSuccess: Becomes true once the data is successfully fetched.
Showing a Loading Skeleton Instead of Plain Text
Instead of just displaying "Loading...", you can show a better UI using skeleton loaders:
.jsx
1if (isLoading) return <div className="skeleton-loader">Loading users...</div>;
2
Retry on Error Handling (retry Option)
By default, React Query retries failed requests 3 times before showing an error. You can customize this behavior:
.jsx
1const { data, isLoading, isError, error } = useQuery({
2queryKey: ["users"],
3queryFn: fetchUsers,
4retry: 2, // Retries twice before failing
5retryDelay: 1000, // 1-second delay before retrying
6});
Showing a Refetch Button on Error
If an API request fails, you can provide users an option to retry manually using refetch()
:
.jsx
1const { data, isLoading, isError, error, refetch } = useQuery({
2queryKey: ["users"],
3queryFn: fetchUsers,
4retry: false, // Disable auto-retry so users can retry manually
5});
6
7if (isError) return (
8<div>
9 <p>Error: {error.message}</p>
10 <button onClick={() => refetch()}>Retry</button>
11</div>
12);
13
React Query Mutation: useMutation
While useQuery is used for fetching and reading data, useMutation
is used for mutating data—such as creating, updating, or deleting resources. It’s particularly useful when you need to send data to the server (e.g., submitting a form or deleting an item) and want to manage the loading, error, and success states of those operations.
Basic Example: Creating Data with useMutation
Here’s an example of using React Query Mutation (useMutation) to create a new post:
.jsx
1import { useMutation } from "@tanstack/react-query";
2import axios from "axios";
3
4// Mutation function
5const createPost = async (newPost) => {
6const response = await axios.post("https://jsonplaceholder.typicode.com/posts", newPost);
7return response.data;
8};
9
10export default function CreatePost() {
11const { mutate, isLoading, isError, error, isSuccess } = useMutation({
12 mutationFn: createPost,
13 onSuccess: (data) => {
14 // This callback runs after a successful mutation
15 console.log("Post created:", data);
16 },
17 onError: (error) => {
18 // This callback runs if the mutation fails
19 console.error("Error:", error);
20 },
21 onSettled: () => {
22 // This callback runs no matter what (success or error)
23 console.log("Mutation finished");
24 },
25});
26
27const handleSubmit = (event) => {
28 event.preventDefault();
29 const newPost = {
30 title: "New Post",
31 body: "This is a new post.",
32 userId: 1,
33 };
34 mutate(newPost); // Trigger the mutation
35};
36
37return (
38 <div>
39 <form onSubmit={handleSubmit}>
40 <button type="submit" disabled={isLoading}>
41 {isLoading ? "Creating..." : "Create Post"}
42 </button>
43 </form>
44 {isSuccess && <p>Post created successfully!</p>}
45 {isError && <p>Error: {error.message}</p>}
46 </div>
47);
48}
Breakdown of useMutation Hook
- mutationFn: This is the function that executes the useMutation (e.g., sending data to the server).
- mutate(): The function you call to trigger the mutation.
- isLoading: Indicates when the mutation is in progress (e.g., waiting for the server response).
- isError: Becomes true if the React Query Mutation fails, and you can access the error object.
- isSuccess: Becomes true if the React Query Mutation is successful.
- onSuccess(): A callback that runs after the React Query Mutation is successful.
- onError(): A callback that runs if the mutation fails.
- onSettled(): Runs whether the mutation succeeds or fails (ideal for resetting the form or triggering other actions).
Updating Data (PUT or PATCH) using useMutation
You can also use react query mutation to update an existing resource:
.jsx
1const updatePost = async (updatedPost) => {
2const response = await axios.put(`https://jsonplaceholder.typicode.com/posts/${updatedPost.id}`, updatedPost);
3return response.data;
4};
5
Deleting Data with useMutation
To delete a resource, just call the appropriate API method:
.jsx
1const deletePost = async (postId) => {
2await axios.delete(`https://jsonplaceholder.typicode.com/posts/${postId}`);
3};
4
5const { mutate } = useMutation({
6mutationFn: deletePost,
7onSuccess: () => {
8 console.log("Post deleted");
9},
10});
Query Caching and Invalidation in React Query
One of the key features of React Query is its automatic caching and invalidation system, which boosts performance by reducing unnecessary API calls and keeping data consistent.
Query Caching
By default, React Query caches all the data fetched through the useQuery hook. This means that if the same query is executed again, React Query will serve the cached data instead of making another network request. This speeds up your app and minimizes redundant calls to the server.
Cache Time and Stale Time
You can control how long React Query caches the data and when the cached data should be considered "stale." By default, React Query keeps data in memory indefinitely, but you can adjust this with the staleTime
and cacheTime
options.
- staleTime: Time before the cached data is considered stale. After this period, React Query will refetch the data on the next render.
- cacheTime: Time before React Query removes the cached data from memory after it becomes unused.
.jsx
1const { data, isLoading } = useQuery({
2queryKey: ["posts"],
3queryFn: fetchPosts,
4staleTime: 10000, // Cache is fresh for 10 seconds
5cacheTime: 60000, // Cache data is kept for 1 minute after it becomes unused
6});
7
Query Invalidation
Query invalidation allows you to manually mark queries as "stale" to trigger a refetch of data. This is useful when performing mutations (like adding, updating, or deleting data) that affect the data shown on the screen. To invalidate a query, use the queryClient.invalidateQueries method. After invalidating the query, React Query will refetch the data associated with that query key.
Example: Invalidating After Mutation
Let's say you create a new post and need to invalidate the posts query to refetch the latest data.
.jsx
1import { useMutation, useQueryClient } from "@tanstack/react-query";
2import axios from "axios";
3
4// Mutation function to create a post
5const createPost = async (newPost) => {
6const response = await axios.post("https://jsonplaceholder.typicode.com/posts", newPost);
7return response.data;
8};
9
10export default function CreatePost() {
11const queryClient = useQueryClient();
12const { mutate } = useMutation(createPost, {
13 onSuccess: () => {
14 // Invalidate the 'posts' query to trigger a refetch
15 queryClient.invalidateQueries(["posts"]);
16 },
17});
18
19const handleSubmit = (event) => {
20 event.preventDefault();
21 const newPost = { title: "New Post", body: "This is a new post", userId: 1 };
22 mutate(newPost);
23};
24
25return (
26 <form onSubmit={handleSubmit}>
27 <button type="submit">Create Post</button>
28 </form>
29);
30}
In this example, after successfully creating a new post, the posts query is invalidated, and React Query will automatically refetch the posts to get the updated list.
React Query Pagination
React Query Pagination helps manage large datasets by splitting them into smaller chunks or 'pages.' It makes adding pagination easy by passing page-specific details like page number and limit to your API.
Example: Paginated Data Fetching
Let’s fetch a list of posts with react query pagination.
.jsx
1import { useQuery } from "@tanstack/react-query";
2import axios from "axios";
3
4const fetchPosts = async (page = 1, limit = 10) => {
5const { data } = await axios.get("https://jsonplaceholder.typicode.com/posts", {
6 params: { _page: page, _limit: limit },
7});
8return data;
9};
10
11export default function PaginatedPosts() {
12const page = 1; // Page number (this can be dynamic)
13const limit = 10; // Number of items per page
14
15const { data, isLoading, isError, error } = useQuery(
16 ["posts", page],
17 () => fetchPosts(page, limit),
18 {
19 keepPreviousData: true, // Keeps previous data when page changes
20 }
21);
22
23if (isLoading) return <p>Loading...</p>;
24if (isError) return <p>Error: {error.message}</p>;
25
26return (
27 <div>
28 <ul>
29 {data.map((post) => (
30 <li key={post.id}>{post.title}</li>
31 ))}
32 </ul>
33
34 {/* Pagination controls */}
35 <button onClick={() => page > 1 && setPage(page - 1)}>Previous</button>
36 <button onClick={() => setPage(page + 1)}>Next</button>
37 </div>
38);
39}
40
Key Options for Pagination
- keepPreviousData: Keeps the previous page’s data helps in react query pagination when fetching a new page, avoiding flashes of empty states.
- Query Key: A unique key for each page to allow caching and proper refetching.
Infinite Queries in React Query
Infinite queries are used when you need to load more data on demand, such as with infinite scrolling. React Query provides a useInfiniteQuery
hook for this purpose, which allows you to fetch paginated data and keep appending new data to the existing list.
Example: Infinite Scrolling with useInfiniteQuery
Let’s implement infinite scrolling with a list of posts.
.jsx
1import { useInfiniteQuery } from "@tanstack/react-query";
2import axios from "axios";
3
4const fetchPosts = async ({ pageParam = 1 }) => {
5const { data } = await axios.get("https://jsonplaceholder.typicode.com/posts", {
6 params: { _page: pageParam, _limit: 10 },
7});
8return data;
9};
10
11export default function InfinitePosts() {
12const {
13 data,
14 isLoading,
15 isError,
16 error,
17 hasNextPage,
18 fetchNextPage,
19 isFetchingNextPage,
20} = useInfiniteQuery(["posts"], fetchPosts, {
21 getNextPageParam: (lastPage, pages) => {
22 // Check if there are more pages to load
23 return lastPage.length === 10 ? pages.length + 1 : undefined;
24 },
25});
26
27if (isLoading) return <p>Loading...</p>;
28if (isError) return <p>Error: {error.message}</p>;
29
30return (
31 <div>
32 <ul>
33 {data.pages.map((page, index) =>
34 page.map((post) => (
35 <li key={post.id}>{post.title}</li>
36 ))
37 )}
38 </ul>
39
40 {/* Load more button */}
41 <button
42 onClick={() => fetchNextPage()}
43 disabled={!hasNextPage || isFetchingNextPage}
44 >
45 {isFetchingNextPage ? "Loading more..." : hasNextPage ? "Load More" : "No more posts"}
46 </button>
47 </div>
48);
49}
Key Concepts of useInfiniteQuery
- getNextPageParam: A function that determines whether there is another page to load. It checks if the last page contains data, and if so, it increments the page number for the next fetch.
- data.pages: The array of pages that have been fetched so far.
- fetchNextPage: A function that loads the next page of data.
- hasNextPage: A boolean indicating whether there is more data to fetch.
- isFetchingNextPage: A boolean indicating whether the next page is being fetched.
Handling Infinite Scrolling with IntersectionObserver
For infinite scrolling where you automatically load more data when the user reaches the end of the list, you can use the IntersectionObserver
API to trigger the fetchNextPage method as the user scrolls.
.jsx
1import { useInfiniteQuery } from "@tanstack/react-query";
2import { useRef, useEffect } from "react";
3import axios from "axios";
4
5const fetchPosts = async ({ pageParam = 1 }) => {
6const { data } = await axios.get("https://jsonplaceholder.typicode.com/posts", {
7 params: { _page: pageParam, _limit: 10 },
8});
9return data;
10};
11
12export default function InfiniteScrollingPosts() {
13const {
14 data,
15 isLoading,
16 isError,
17 error,
18 fetchNextPage,
19 isFetchingNextPage,
20 hasNextPage,
21} = useInfiniteQuery(["posts"], fetchPosts, {
22 getNextPageParam: (lastPage, pages) => {
23 return lastPage.length === 10 ? pages.length + 1 : undefined;
24 },
25});
26
27const loadMoreRef = useRef(null);
28
29useEffect(() => {
30 const observer = new IntersectionObserver(
31 ([entry]) => {
32 if (entry.isIntersecting && hasNextPage && !isFetchingNextPage) {
33 fetchNextPage();
34 }
35 },
36 {
37 rootMargin: "100px",
38 }
39 );
40
41 if (loadMoreRef.current) {
42 observer.observe(loadMoreRef.current);
43 }
44
45 return () => {
46 if (loadMoreRef.current) {
47 observer.disconnect();
48 }
49 };
50}, [hasNextPage, isFetchingNextPage, fetchNextPage]);
51
52if (isLoading) return <p>Loading...</p>;
53if (isError) return <p>Error: {error.message}</p>;
54
55return (
56 <div>
57 <ul>
58 {data.pages.map((page, index) =>
59 page.map((post) => (
60 <li key={post.id}>{post.title}</li>
61 ))
62 )}
63 </ul>
64
65 {/* The invisible trigger for loading more posts */}
66 <div ref={loadMoreRef}></div>
67 </div>
68);
69}
70
Here, the IntersectionObserver is used to automatically load more data when the "load more" button reaches the viewport.
Frequently Asked Questions
-
Q: Can I use React Query with Next.js?
A: Yes, React Query works seamlessly with Next.js. You can integrate it with server-side rendering (SSR) and static site generation (SSG) to fetch and cache data at the page level, providing a better user experience and improved performance. Learn More About SSR ans SSG in Next.js.
-
Q: What is the difference between React Query vs SWR?
A: React Query vs SWR are both data-fetching libraries, but they differ in their feature and approach. React Query offers more advanced features like built-in DevTools, background refetching, and pagination hooks (useInfiniteQuery), while SWR focuses on minimalistic data fetching. React Query also handles mutations and cache management more comprehensively, making it better for complex apps.
-
Q: Can React Query replace Redux?
A: **No—React Query manages server-state (APIs, cached data), while Redux handles client-state (UI state, themes). They complement each other but can reduce Redux usage significantly.
-
Q: How do I reset the React Query cache?
A: Use queryClient.resetQueries() or queryClient.clear() to wipe react query cache.
Conclusion
In this guide, we've learned how React Query can simplify data fetching and management in your React applications. We covered key features like react query useQuery, react query cache, react query mutation, react query pagination react query devtools and difference between react query vs swr. Whether you’re building a simple CRUD app or a complex interface, React Query makes managing server state easy and efficient. Start using these features today, and see your React apps become faster and more user-friendly!