Towards Dev

A publication for sharing projects, ideas, codes, and new theories.

Follow publication

React Query in a Next JS App — A Complete Guide

Bruno Feres
Towards Dev
Published in
4 min readFeb 17, 2025

Managing server-state in React applications is always a challenge. The most common approaches, like using useEffectand 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 🚀

Published in Towards Dev

A publication for sharing projects, ideas, codes, and new theories.

Written by Bruno Feres

Sr. Software Engineer, obsessed reader and silly jokes teller.

No responses yet

Write a response