React Query in a Next JS App — A Complete Guide

Managing server-state in React applications is always a challenge. The most common approaches, like using useEffect
and useState,
for data fetching come with limitations like unnecessary re-fetching and complex caching strategies. This is where React Query comes in — a powerful library that simplifies data fetching, caching, and synchronization.
In this article, we’ll explore how to integrate React Query into a Next JS app, covering:
- Installation and setup
- Fetching and caching data
- Handling mutations
- Implementing infinite queries
Why Use React Query in Next JS?
Next JS has features like getServerSideProps and getStaticProps for fetching data, but they’re not great for managing client-side state.
React Query fills this gap by providing a way to fetch data and manage client-side state.
React Query will help us by fitting the gap with:
- Automatically caching and syncing server-state
- Eliminating the need for
useEffect
for data fetching - Supporting background re-fetching and pagination
- Reducing unnecessary API requests with smart caching
Installing React Query
To get started, install React Query:
yarn add @tanstack/react-query
Setting Up React Query in Your Next JS App
To use React Query globally, wrap your application with QueryClientProvider
in _app.js
or _app.tsx
.
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const queryClient = new QueryClient();
function MyApp({ Component, pageProps }) {
return (
<QueryClientProvider client={queryClient}>
<Component {...pageProps} />
</QueryClientProvider>
);
}
export default MyApp;
Now, React Query is available throughout your app! 😎
Fetching Data with useQuery
React Query provides useQuery
to fetch and cache data efficiently.
Imagine that you have a Posts API. You would fetch your data like:
import { useQuery } from '@tanstack/react-query';
const fetchPosts = async () => {
const res = await fetch('https://jsonplaceholder.typicode.com/posts');
return res.json();
};
export default function Posts() {
const { data, error, isLoading } = useQuery(['posts'], fetchPosts);
if (isLoading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<div>
<h1>Posts</h1>
<ul>
{data.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
);
}
Understanding the structure of the useQuery hook
useQuery requires 2 mandatory params, the query key and the function that fetches the data. The query key used will be the cache key, so caching will rely on this key. You can pass other values to the key array, including variables like page or search. This way whenever page or search changes, react query will automatically fetch data with the new params and cache it.
useQuery(['queryKey', variable1, variable2], functionToFetchData)
Additional params
You can pass additional params to the useQuery hook.
Enabled
“Enabled” allows React Query to know when it should attempt to fetch data. For instance, if your data fetch depends on a dynamically set variable, you can pass this variable with two bangs, indicating that the query should be enabled when the variable is present.
const { data, isLoading, error } = useQuery({
queryKey: ['posts', page],
queryFn: fetchPosts,
enabled: !!authorId, // Fetch only if `authorId` is available
});
keepPreviousData
“keepPreviousData” is a useful feature in React Query that allows you to specify whether data should be retained after a query key change. This means that if you need to show the same data again, you don’t have to refetch it from the server. For instance, let’s say you have three pages of posts, and your user clicks on page 2, fetching the posts for that page. If your user then clicks back to page 1, you can present the data from page 1 without having to refetch it from the server.
const { data, isLoading, error } = useQuery({
queryKey: ['posts', page],
queryFn: fetchPosts,
keepPreviousData: true, // Keeps previous data when changing queries (e.g., pagination)
});
refetchOnWindowFocus
refetchOnWindowFocus is a React Query feature that determines whether data should be fetched again whenever a user’s site page gains focus. For instance, if a user switches between tabs, React Query will refresh the data. This is particularly useful for volatile data, such as cryptocurrency prices.
const { data, isLoading, error } = useQuery({
queryKey: ['posts', page],
queryFn: fetchPosts,
refetchOnWindowFocus: false,
});
Mutate Data with useMutation
React Query also makes handling mutations (POST, PUT, DELETE requests) easy with useMutation
.
Adding a new post.
import { useMutation, useQueryClient } from '@tanstack/react-query';
const createPost = async (newPost) => {
const res = await fetch('https://jsonplaceholder.typicode.com/posts', {
method: 'POST',
body: JSON.stringify(newPost),
headers: { 'Content-Type': 'application/json' },
});
return res.json();
};
export default function AddPost() {
const queryClient = useQueryClient();
const mutation = useMutation(createPost, {
onSuccess: () => {
queryClient.invalidateQueries(['posts']); // Refetch posts after adding a new one
},
});
return (
<button onClick={() => mutation.mutate({ title: 'New Post', body: 'Content' })}>
Add Post
</button>
);
}
Isn’t it easy?
✨ Bônus — Implement Infinite Scrolling with useInfiniteQuery
For paginated data, React Query provides useInfiniteQuery
.
Fetching paginated posts.
import { useInfiniteQuery } from '@tanstack/react-query';
const fetchPosts = async ({ pageParam = 1 }) => {
const res = await fetch(`https://jsonplaceholder.typicode.com/posts?_page=${pageParam}`);
return res.json();
};
export default function InfinitePosts() {
const { data, fetchNextPage, hasNextPage } = useInfiniteQuery(
['posts'],
fetchPosts,
{
getNextPageParam: (lastPage, pages) => pages.length + 1,
}
);
return (
<div>
{data?.pages.map((page, i) => (
<div key={i}>
{page.map((post) => (
<p key={post.id}>{post.title}</p>
))}
</div>
))}
{hasNextPage && <button onClick={() => fetchNextPage()}>Load More</button>}
</div>
);
}
Conclusion
React Query makes data fetching in Next JS apps easier by handling caching, background re-fetching, and mutations efficiently. With its simple API and powerful features, it significantly improves performance and the developer experience.
If you haven’t used React Query in your projects yet, now is the time to give it a try!
If you found this guide helpful, feel free to share it and follow me for more React tutorials!
Respect and enjoy the peace 🚀