Next.js React Query
Introduction
React Query, now part of the TanStack collection as TanStack Query, is a powerful data fetching and state management library that works excellently with Next.js applications. It provides a robust solution for handling server state, offering features like caching, background updates, and automatic refetching.
In this tutorial, we'll explore how to integrate React Query with Next.js to create efficient, responsive applications with optimized data fetching capabilities.
What is React Query?
React Query solves a fundamental challenge in React and Next.js applications: managing server state. While tools like Redux or Context API are great for managing client state, server state has unique characteristics:
- Server data is stored remotely
- Requires asynchronous APIs for fetching
- Can become stale and need refreshing
- Can be updated by multiple users simultaneously
React Query provides a comprehensive solution to these challenges, offering:
- Automatic caching and stale data management
- Background data refetching
- Loading and error states
- Pagination and infinite scrolling support
- Data prefetching
- Mutation capabilities (updating server data)
Getting Started with React Query in Next.js
Installation
First, let's install React Query in your Next.js project:
npm install @tanstack/react-query
# or
yarn add @tanstack/react-query
Setting Up the React Query Provider
For React Query to work across your application, you need to set up a provider at the root level. In Next.js, this is typically done in your _app.js
or app.js
file:
// pages/_app.js (for Pages Router)
// or app/providers.jsx (for App Router)
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { useState } from 'react';
export default function App({ Component, pageProps }) {
const [queryClient] = useState(() => new QueryClient());
return (
<QueryClientProvider client={queryClient}>
<Component {...pageProps} />
</QueryClientProvider>
);
}
For App Router, you'd create a client component provider:
// app/providers.jsx
'use client'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { useState } from 'react';
export function Providers({ children }) {
const [queryClient] = useState(() => new QueryClient());
return (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
);
}
And then use it in your layout:
// app/layout.jsx
import { Providers } from './providers';
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}
Basic Data Fetching with React Query
Let's explore the most basic use case: fetching data from an API.
The useQuery
Hook
The main hook from React Query for fetching data is useQuery
. Here's a simple example:
import { useQuery } from '@tanstack/react-query';
function fetchPosts() {
return fetch('https://jsonplaceholder.typicode.com/posts')
.then(response => response.json());
}
function PostsList() {
const { data, isLoading, error } = useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
});
if (isLoading) return <div>Loading posts...</div>;
if (error) return <div>Error loading posts: {error.message}</div>;
return (
<div>
<h1>Posts</h1>
<ul>
{data.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
);
}
Understanding Query Keys
The queryKey
is a unique identifier for your query. React Query uses this key to:
- Cache query results
- Share queries across components
- Refetch or invalidate queries
Query keys can be simple strings or arrays for more complex queries:
// Simple key
useQuery({ queryKey: ['todos'], queryFn: fetchTodos });
// Key with parameters
useQuery({
queryKey: ['todo', { id: 5 }],
queryFn: () => fetchTodoById(5)
});
Advanced React Query Features
Caching and Stale Time
React Query automatically caches query results. You can control caching behavior with options:
const { data } = useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
staleTime: 1000 * 60 * 5, // Data is fresh for 5 minutes
cacheTime: 1000 * 60 * 30, // Data remains in cache for 30 minutes
});
Dependent Queries
Sometimes, you need one query to complete before running another:
function UserPosts() {
const { data: user } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
});
const { data: posts } = useQuery({
queryKey: ['posts', user?.id],
queryFn: () => fetchPostsByUser(user.id),
// Only run this query when user.id exists
enabled: !!user?.id,
});
// Render user posts
}
Mutations with useMutation
To modify server data, use the useMutation
hook:
import { useMutation, useQueryClient } from '@tanstack/react-query';
function CreatePost() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (newPost) => {
return fetch('https://jsonplaceholder.typicode.com/posts', {
method: 'POST',
body: JSON.stringify(newPost),
headers: {
'Content-type': 'application/json; charset=UTF-8',
},
}).then(response => response.json());
},
onSuccess: (data) => {
// Invalidate and refetch the posts list
queryClient.invalidateQueries({ queryKey: ['posts'] });
// Or update the cache directly
queryClient.setQueryData(['posts'], (oldData) => {
return [...oldData, data];
});
},
});
const handleSubmit = (event) => {
event.preventDefault();
const formData = new FormData(event.target);
mutation.mutate({
title: formData.get('title'),
body: formData.get('body'),
userId: 1,
});
};
return (
<form onSubmit={handleSubmit}>
<input name="title" placeholder="Post title" />
<textarea name="body" placeholder="Post content" />
<button type="submit" disabled={mutation.isPending}>
{mutation.isPending ? 'Creating...' : 'Create Post'}
</button>
</form>
);
}
React Query with Next.js Server Components
With Next.js App Router and Server Components, we need to be careful about where React Query is used since it's a client-side library.
Keeping React Query in Client Components
Create a client component that uses React Query:
// app/posts/posts-list.jsx
'use client'
import { useQuery } from '@tanstack/react-query';
export default function PostsList() {
const { data, isLoading } = useQuery({
queryKey: ['posts'],
queryFn: () => fetch('/api/posts').then(res => res.json()),
});
if (isLoading) return <div>Loading...</div>;
return (
<ul>
{data.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
Then import it into your server component:
// app/posts/page.jsx
import PostsList from './posts-list';
export default function PostsPage() {
return (
<div>
<h1>Posts</h1>
<PostsList />
</div>
);
}
Hydration Strategy with Next.js
A common pattern is to prefetch data on the server and hydrate React Query on the client:
// app/api/posts/route.js
export async function GET() {
const posts = await fetchPosts(); // Your data fetching logic
return Response.json(posts);
}
// app/posts/page.jsx
import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query';
import PostsList from './posts-list';
export default async function PostsPage() {
const queryClient = new QueryClient();
await queryClient.prefetchQuery({
queryKey: ['posts'],
queryFn: () => fetch('http://localhost:3000/api/posts').then(res => res.json()),
});
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<div>
<h1>Posts</h1>
<PostsList />
</div>
</HydrationBoundary>
);
}
Real-World Application: Building a Blog
Let's build a simple blog application using React Query and Next.js:
// app/blog/post-list.jsx
'use client'
import { useQuery } from '@tanstack/react-query';
import Link from 'next/link';
export default function PostList() {
const { data, isLoading, error } = useQuery({
queryKey: ['posts'],
queryFn: () => fetch('/api/posts').then(res => res.json()),
});
if (isLoading) return <div>Loading posts...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div className="grid gap-4">
{data.map(post => (
<div key={post.id} className="border p-4 rounded-md">
<h2 className="text-xl font-bold">{post.title}</h2>
<p className="text-gray-600">{post.excerpt}</p>
<Link href={`/blog/${post.slug}`} className="text-blue-500">
Read more
</Link>
</div>
))}
</div>
);
}
For the single post page:
// app/blog/[slug]/post.jsx
'use client'
import { useQuery } from '@tanstack/react-query';
export default function Post({ slug }) {
const { data: post, isLoading, error } = useQuery({
queryKey: ['post', slug],
queryFn: () => fetch(`/api/posts/${slug}`).then(res => res.json()),
});
if (isLoading) return <div>Loading post...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<article className="prose max-w-prose mx-auto">
<h1>{post.title}</h1>
<div className="text-gray-500">Published on {post.date}</div>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
);
}
And the route handler:
// app/blog/[slug]/page.jsx
import Post from './post';
export default function PostPage({ params }) {
return <Post slug={params.slug} />;
}
Optimizing React Query Performance
Query Options for Performance
const { data } = useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
// Refetch options
refetchOnWindowFocus: false, // Don't refetch when window regains focus
refetchOnMount: false, // Don't refetch when component mounts
staleTime: Infinity, // Data is never considered stale
});
Pagination
function PaginatedPosts() {
const [page, setPage] = useState(1);
const { data, isLoading } = useQuery({
queryKey: ['posts', page],
queryFn: () => fetchPosts(page),
keepPreviousData: true, // Keep showing previous data while loading next page
});
return (
<div>
{/* Post list */}
<div className="flex gap-2">
<button
onClick={() => setPage(old => Math.max(old - 1, 1))}
disabled={page === 1}
>
Previous
</button>
<span>Page {page}</span>
<button
onClick={() => setPage(old => old + 1)}
disabled={!data?.hasMore}
>
Next
</button>
</div>
</div>
);
}
Infinite Queries
import { useInfiniteQuery } from '@tanstack/react-query';
function InfinitePosts() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useInfiniteQuery({
queryKey: ['infinitePosts'],
queryFn: ({ pageParam = 1 }) => fetchPosts(pageParam),
getNextPageParam: (lastPage) => lastPage.nextPage,
});
return (
<div>
{data?.pages.map((group, i) => (
<div key={i}>
{group.posts.map(post => (
<div key={post.id}>{post.title}</div>
))}
</div>
))}
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
>
{isFetchingNextPage
? 'Loading more...'
: hasNextPage
? 'Load more'
: 'Nothing more to load'}
</button>
</div>
);
}
Summary
React Query is an excellent tool for managing server state in Next.js applications, offering:
- Efficient data fetching with automatic caching
- Easy loading and error states
- Background updates and refetching
- Powerful mutation capabilities
- Pagination and infinite scrolling support
When using React Query with Next.js, particularly with the App Router and server components, remember to:
- Keep React Query in client components
- Use the
'use client'
directive when working with React Query hooks - Consider hydration strategies for prefetched data
By integrating React Query into your Next.js application, you can create a more responsive, efficient user experience with optimized data fetching.
Additional Resources
Exercises
- Create a simple Next.js application that displays a list of users fetched from an API using React Query
- Add pagination or infinite scrolling to your user list
- Implement a form to create or update user data using
useMutation
- Build a data-dependent view where one query depends on the result of another
- Optimize your React Query implementation by configuring appropriate staleTime and cacheTime settings
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)