1. 引言

在现代前端开发中,数据获取和缓存管理是构建高性能应用的关键。Next.js作为流行的React框架,提供了强大的服务端渲染能力,而React Query则是一个专门用于管理服务器状态的数据获取库。将这两者结合使用,可以创建出既快速又响应灵敏的Web应用。本文将深入探讨如何有效地结合Next.js和React Query,优化前端数据获取与缓存策略。

2. Next.js与React Query简介

2.1 Next.js概述

Next.js是一个基于React的框架,它提供了服务端渲染(SSR)、静态站点生成(SSG)、API路由等功能,使得开发者能够构建高性能的React应用。Next.js的数据获取方法包括:

  • getStaticProps: 在构建时获取数据,用于静态生成
  • getServerSideProps: 在每次请求时获取数据,用于服务端渲染
  • getStaticPaths: 用于动态路由的静态生成

2.2 React Query概述

React Query(现在称为TanStack Query)是一个用于管理、缓存和同步异步数据的状态管理库。它提供了以下核心功能:

  • 数据获取、缓存和同步
  • 后台数据更新和重新获取
  • 分页和延迟加载
  • 乐观更新
  • 并发查询

React Query解决了传统React应用中数据获取和状态管理的痛点,如数据缓存、重复请求、过时数据处理等。

3. 为什么需要结合Next.js和React Query

Next.js和React Query各有优势,结合使用可以发挥它们的最大潜力:

3.1 互补的数据获取策略

Next.js提供了服务端数据获取能力,适合首屏渲染和SEO优化;React Query则擅长客户端数据管理和缓存,适合用户交互后的数据更新。两者结合可以实现:

  • 首屏快速加载(通过Next.js的SSR/SSG)
  • 客户端无缝数据更新(通过React Query)
  • 减少重复请求(通过React Query的缓存机制)

3.2 改善用户体验

  • 页面加载更快(服务端渲染)
  • 导航更流畅(客户端缓存)
  • 离线体验更好(数据持久化)

3.3 优化开发体验

  • 简化数据获取逻辑
  • 减少样板代码
  • 统一错误处理和加载状态管理

4. 在Next.js项目中集成React Query

4.1 安装React Query

首先,我们需要在Next.js项目中安装React Query:

npm install @tanstack/react-query # 或者 yarn add @tanstack/react-query 

4.2 配置QueryClient

在Next.js应用中,我们需要创建一个QueryClient实例,并将其提供给整个应用。由于Next.js支持服务端渲染,我们需要确保QueryClient在服务端和客户端都能正常工作。

创建app/providers.tsx文件:

'use client'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { useState } from 'react'; export default function Providers({ children }: { children: React.ReactNode }) { // 使用useState确保QueryClient在客户端不会重新创建 const [queryClient] = useState( () => new QueryClient({ defaultOptions: { queries: { // 在客户端,默认禁用服务端获取的查询的重新获取 staleTime: 60 * 1000, // 1分钟 refetchOnMount: false, refetchOnWindowFocus: false, }, }, }) ); return ( <QueryClientProvider client={queryClient}> {children} </QueryClientProvider> ); } 

4.3 在应用中提供QueryClient

修改app/layout.tsx文件,将Providers组件添加到应用中:

import Providers from './providers'; export default function RootLayout({ children, }: { children: React.ReactNode; }) { return ( <html lang="en"> <body> <Providers>{children}</Providers> </body> </html> ); } 

4.4 配置React Query Devtools(开发环境)

为了在开发过程中更好地调试React Query,我们可以添加React Query Devtools:

'use client'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; import { useState } from 'react'; export default function Providers({ children }: { children: React.ReactNode }) { const [queryClient] = useState(() => new QueryClient()); return ( <QueryClientProvider client={queryClient}> {children} <ReactQueryDevtools initialIsOpen={false} /> </QueryClientProvider> ); } 

5. 使用React Query优化数据获取

5.1 基本数据获取

在Next.js中,我们可以使用React Query的useQuery钩子来获取数据。下面是一个基本的例子:

import { useQuery } from '@tanstack/react-query'; // 定义数据获取函数 const fetchPosts = async () => { const response = await fetch('https://jsonplaceholder.typicode.com/posts'); if (!response.ok) { throw new Error('Network response was not ok'); } return response.json(); }; // 在组件中使用 function Posts() { const { data, error, isLoading } = useQuery({ queryKey: ['posts'], queryFn: fetchPosts, }); if (isLoading) return <div>Loading...</div>; if (error) return <div>An error occurred: {error.message}</div>; return ( <ul> {data.map((post: any) => ( <li key={post.id}>{post.title}</li> ))} </ul> ); } 

5.2 结合Next.js服务端数据获取

为了充分利用Next.js的服务端渲染能力,我们可以将React Query与Next.js的数据获取方法结合起来。下面是一个结合getServerSideProps和React Query的例子:

// pages/posts/[id].tsx import { dehydrate, QueryClient, useQuery } from '@tanstack/react-query'; import { GetServerSideProps } from 'next'; // 数据获取函数 const fetchPost = async (id: string) => { const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`); if (!response.ok) { throw new Error('Network response was not ok'); } return response.json(); }; // 组件 function Post({ id }: { id: string }) { const { data, error, isLoading } = useQuery({ queryKey: ['post', id], queryFn: () => fetchPost(id), // 由于数据已经在服务端预取,这里不需要重新获取 initialData: () => { // 从dehydrated state中获取数据 const queryClient = new QueryClient(); return queryClient.getQueryData(['post', id]); }, }); if (isLoading) return <div>Loading...</div>; if (error) return <div>An error occurred: {error.message}</div>; return ( <div> <h1>{data.title}</h1> <p>{data.body}</p> </div> ); } // 服务端预取数据 export const getServerSideProps: GetServerSideProps = async (context) => { const { id } = context.params; const queryClient = new QueryClient(); // 在服务端预取数据 await queryClient.prefetchQuery({ queryKey: ['post', id], queryFn: () => fetchPost(id as string), }); return { props: { id, // 将dehydrated state传递给客户端 dehydratedState: dehydrate(queryClient), }, }; }; export default Post; 

5.3 使用Hydrate组件

在Next.js中,我们可以使用Hydrate组件来将服务端获取的数据传递给客户端。修改app/providers.tsx

'use client'; import { QueryClient, QueryClientProvider, Hydrate } from '@tanstack/react-query'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; import { useState } from 'react'; export default function Providers({ children, dehydratedState }: { children: React.ReactNode; dehydratedState: any }) { const [queryClient] = useState(() => new QueryClient()); return ( <QueryClientProvider client={queryClient}> <Hydrate state={dehydratedState}> {children} </Hydrate> <ReactQueryDevtools initialIsOpen={false} /> </QueryClientProvider> ); } 

然后在app/layout.tsx中传递dehydratedState

import Providers from './providers'; export default function RootLayout({ children, dehydratedState, }: { children: React.ReactNode; dehydratedState: any; }) { return ( <html lang="en"> <body> <Providers dehydratedState={dehydratedState}> {children} </Providers> </body> </html> ); } 

6. 缓存策略的实现

6.1 理解React Query的缓存机制

React Query使用基于键的缓存系统,每个查询都有一个唯一的键(通常是数组形式)。当数据被获取后,它会被存储在缓存中,并在需要时被重用。

6.2 配置缓存策略

我们可以通过配置QueryClient来定义全局的缓存策略:

const queryClient = new QueryClient({ defaultOptions: { queries: { // 数据被认为是"新鲜"的时间(毫秒) staleTime: 1000 * 60 * 5, // 5分钟 // 数据在缓存中保留的时间(毫秒) cacheTime: 1000 * 60 * 30, // 30分钟 // 窗口获得焦点时是否重新获取数据 refetchOnWindowFocus: false, // 网络重连时是否重新获取数据 refetchOnReconnect: true, // 组件挂载时是否重新获取数据 refetchOnMount: true, }, }, }); 

6.3 针对特定查询的缓存配置

我们也可以为特定查询配置缓存策略:

const { data } = useQuery({ queryKey: ['user', userId], queryFn: () => fetchUser(userId), staleTime: 1000 * 60 * 10, // 10分钟 cacheTime: 1000 * 60 * 60, // 1小时 refetchOnWindowFocus: false, }); 

6.4 手动缓存管理

React Query提供了手动管理缓存的方法:

import { useQueryClient } from '@tanstack/react-query'; function MyComponent() { const queryClient = useQueryClient(); // 手动设置查询数据 const setUserData = () => { queryClient.setQueryData(['user', 1], { name: 'John Doe', email: 'john@example.com' }); }; // 手动获取查询数据 const getUserData = () => { return queryClient.getQueryData(['user', 1]); }; // 手动移除查询数据 const removeUserData = () => { queryClient.removeQueries(['user', 1]); }; // 使查询数据失效(触发重新获取) const invalidateUserData = () => { queryClient.invalidateQueries(['user', 1]); }; return ( <div> <button onClick={setUserData}>Set User Data</button> <button onClick={getUserData}>Get User Data</button> <button onClick={removeUserData}>Remove User Data</button> <button onClick={invalidateUserData}>Invalidate User Data</button> </div> ); } 

6.5 持久化缓存

为了在页面刷新或用户重新访问时保留缓存数据,我们可以将缓存持久化到本地存储:

import { QueryClient } from '@tanstack/react-query'; import { persistQueryClient } from '@tanstack/react-query-persist-client'; import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister'; const queryClient = new QueryClient({ defaultOptions: { queries: { cacheTime: 1000 * 60 * 60 * 24, // 24小时 }, }, }); const localStoragePersister = createSyncStoragePersister({ storage: window.localStorage, }); persistQueryClient({ queryClient, persister: localStoragePersister, maxAge: 1000 * 60 * 60 * 24, // 24小时 buster: '', // 可以设置为一个版本号,用于在更新时清除缓存 }); 

7. 实际应用场景和代码示例

7.1 分页数据获取

分页是Web应用中常见的需求,React Query提供了处理分页数据的便捷方法:

import { useQuery, useInfiniteQuery } from '@tanstack/react-query'; // 使用useQuery实现分页 function PaginatedPosts({ page }: { page: number }) { const { data, error, isLoading } = useQuery({ queryKey: ['posts', page], queryFn: () => fetchPosts(page), keepPreviousData: true, // 保持上一页的数据,提供更好的用户体验 }); if (isLoading) return <div>Loading...</div>; if (error) return <div>An error occurred: {error.message}</div>; return ( <div> <ul> {data.map((post: any) => ( <li key={post.id}>{post.title}</li> ))} </ul> <button disabled={page === 1} onClick={() => setPage(page - 1)}> Previous </button> <button onClick={() => setPage(page + 1)}>Next</button> </div> ); } // 使用useInfiniteQuery实现无限滚动 function InfinitePosts() { const { data, error, isLoading, fetchNextPage, hasNextPage, isFetchingNextPage, } = useInfiniteQuery({ queryKey: ['posts'], queryFn: ({ pageParam = 1 }) => fetchPosts(pageParam), getNextPageParam: (lastPage, allPages) => { // 如果还有更多数据,返回下一页的页码,否则返回undefined return lastPage.length > 0 ? allPages.length + 1 : undefined; }, }); if (isLoading) return <div>Loading...</div>; if (error) return <div>An error occurred: {error.message}</div>; return ( <div> <ul> {data.pages.map((page, i) => ( <React.Fragment key={i}> {page.map((post: any) => ( <li key={post.id}>{post.title}</li> ))} </React.Fragment> ))} </ul> <button onClick={() => fetchNextPage()} disabled={!hasNextPage || isFetchingNextPage} > {isFetchingNextPage ? 'Loading more...' : hasNextPage ? 'Load More' : 'Nothing more to load'} </button> </div> ); } 

7.2 乐观更新

乐观更新是一种在等待服务器响应时先更新UI的技术,可以提供更快的用户体验:

import { useMutation, useQueryClient } from '@tanstack/react-query'; function AddPostForm() { const [title, setTitle] = useState(''); const queryClient = useQueryClient(); const mutation = useMutation({ mutationFn: (newPost: { title: string }) => { return fetch('https://jsonplaceholder.typicode.com/posts', { method: 'POST', body: JSON.stringify(newPost), headers: { 'Content-type': 'application/json; charset=UTF-8', }, }).then((res) => res.json()); }, onMutate: async (newPost) => { // 取消任何正在进行的查询 await queryClient.cancelQueries({ queryKey: ['posts'] }); // 获取当前数据的快照 const previousPosts = queryClient.getQueryData(['posts']); // 乐观更新 queryClient.setQueryData(['posts'], (old: any) => [ ...old, { ...newPost, id: Date.now() }, ]); // 返回包含快照的上下文对象 return { previousPosts }; }, onError: (err, newPost, context) => { // 如果发生错误,回滚到之前的值 queryClient.setQueryData(['posts'], context?.previousPosts); }, onSettled: () => { // 无论成功或失败,都重新获取数据以确保一致性 queryClient.invalidateQueries({ queryKey: ['posts'] }); }, }); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); mutation.mutate({ title }); setTitle(''); }; return ( <form onSubmit={handleSubmit}> <input type="text" value={title} onChange={(e) => setTitle(e.target.value)} placeholder="Post title" /> <button type="submit" disabled={mutation.isLoading}> {mutation.isLoading ? 'Adding...' : 'Add Post'} </button> {mutation.isError && <div>Error adding post: {mutation.error.message}</div>} </form> ); } 

7.3 依赖查询

有时候,一个查询可能依赖于另一个查询的结果。React Query提供了enabled选项来处理这种情况:

function UserProfile({ userId }: { userId: string }) { // 获取用户基本信息 const { data: user, isLoading: isUserLoading } = useQuery({ queryKey: ['user', userId], queryFn: () => fetchUser(userId), }); // 获取用户的帖子,依赖于用户查询的结果 const { data: posts, isLoading: isPostsLoading } = useQuery({ queryKey: ['posts', userId], queryFn: () => fetchUserPosts(userId), // 只有当user数据存在时才执行查询 enabled: !!user, }); if (isUserLoading) return <div>Loading user...</div>; if (!user) return <div>User not found</div>; return ( <div> <h1>{user.name}</h1> <p>{user.email}</p> <h2>Posts</h2> {isPostsLoading ? ( <div>Loading posts...</div> ) : ( <ul> {posts.map((post: any) => ( <li key={post.id}>{post.title}</li> ))} </ul> )} </div> ); } 

7.4 并行查询

React Query允许并行执行多个查询,可以使用useQueries钩子:

function Dashboard() { const userId = '1'; const results = useQueries({ queries: [ { queryKey: ['user', userId], queryFn: () => fetchUser(userId), }, { queryKey: ['posts', userId], queryFn: () => fetchUserPosts(userId), }, { queryKey: ['comments', userId], queryFn: () => fetchUserComments(userId), }, ], }); const [user, posts, comments] = results; if (results.some((result) => result.isLoading)) { return <div>Loading dashboard...</div>; } if (results.some((result) => result.isError)) { return <div>Error loading dashboard</div>; } return ( <div> <h1>Welcome, {user.data.name}!</h1> <section> <h2>Your Posts</h2> <ul> {posts.data.map((post: any) => ( <li key={post.id}>{post.title}</li> ))} </ul> </section> <section> <h2>Your Comments</h2> <ul> {comments.data.map((comment: any) => ( <li key={comment.id}>{comment.body}</li> ))} </ul> </section> </div> ); } 

7.5 预取数据

为了提高用户体验,我们可以预取用户可能需要的数据:

import { useQuery, useQueryClient } from '@tanstack/react-query'; function UserList() { const { data: users } = useQuery({ queryKey: ['users'], queryFn: fetchUsers, }); const queryClient = useQueryClient(); const prefetchUser = (userId: string) => { queryClient.prefetchQuery({ queryKey: ['user', userId], queryFn: () => fetchUser(userId), staleTime: 1000 * 60 * 5, // 5分钟 }); }; return ( <div> <h1>Users</h1> <ul> {users.map((user: any) => ( <li key={user.id} onMouseEnter={() => prefetchUser(user.id)} > <Link href={`/users/${user.id}`}> {user.name} </Link> </li> ))} </ul> </div> ); } 

8. 性能优化技巧

8.1 选择性重新获取

React Query允许我们配置在特定条件下重新获取数据:

const { data } = useQuery({ queryKey: ['posts'], queryFn: fetchPosts, // 仅在用户重新连接到互联网时重新获取 refetchOnReconnect: true, // 不在窗口获得焦点时重新获取 refetchOnWindowFocus: false, // 不在组件挂载时重新获取 refetchOnMount: false, }); 

8.2 轮询数据

对于需要实时更新的数据,我们可以使用轮询:

const { data } = useQuery({ queryKey: ['messages'], queryFn: fetchMessages, // 每5秒重新获取一次数据 refetchInterval: 5000, // 当窗口不在焦点时停止轮询 refetchIntervalInBackground: false, }); 

8.3 窗口焦点重新获取

我们可以配置在窗口获得焦点时重新获取数据:

const { data } = useQuery({ queryKey: ['messages'], queryFn: fetchMessages, // 窗口获得焦点时重新获取数据 refetchOnWindowFocus: true, // 可以自定义重新获取的延迟时间(毫秒) refetchOnWindowFocus: 1000, }); 

8.4 网络重连重新获取

当用户重新连接到互联网时,我们可以重新获取数据:

const { data } = useQuery({ queryKey: ['messages'], queryFn: fetchMessages, // 网络重连时重新获取数据 refetchOnReconnect: true, }); 

8.5 分页和无限滚动优化

对于分页和无限滚动,我们可以使用placeholderData来提供更好的用户体验:

const { data } = useQuery({ queryKey: ['posts', page], queryFn: () => fetchPosts(page), // 保持上一页的数据,避免闪烁 keepPreviousData: true, // 提供占位数据 placeholderData: (previousData) => previousData, }); 

8.6 查询取消

为了避免不必要的网络请求,我们可以取消正在进行的查询:

import { useQuery } from '@tanstack/react-query'; import axios from 'axios'; function Component() { const { data } = useQuery({ queryKey: ['data'], queryFn: async ({ signal }) => { // 将signal传递给axios,以便在组件卸载时取消请求 const response = await axios.get('/api/data', { signal }); return response.data; }, }); // ... } 

8.7 查询重试

我们可以配置查询失败时的重试行为:

const { data } = useQuery({ queryKey: ['data'], queryFn: fetchData, // 失败时重试3次 retry: 3, // 重试之间的延迟(毫秒) retryDelay: 1000, // 自定义重试逻辑 retry: (failureCount, error) => { if (error.status === 404) return false; // 404错误不重试 return failureCount < 3; }, }); 

9. 常见问题和解决方案

9.1 处理过时数据

问题:数据显示过时,没有及时更新。

解决方案:

const { data } = useQuery({ queryKey: ['data'], queryFn: fetchData, // 减少staleTime,使数据更快变为过时状态 staleTime: 1000, // 1秒后数据变为过时 // 增加重新获取的频率 refetchInterval: 5000, // 每5秒重新获取一次 }); 

9.2 处理大型数据集

问题:获取大型数据集导致性能问题。

解决方案:

const { data } = useQuery({ queryKey: ['large-dataset'], queryFn: fetchLargeDataset, // 使用select只选择需要的数据 select: (data) => data.items, // 使用placeholderData提供初始数据 placeholderData: { items: [] }, }); 

9.3 处理依赖查询

问题:一个查询依赖于另一个查询的结果,但嵌套使用useQuery导致代码复杂。

解决方案:

function Component() { // 第一个查询 const { data: user } = useQuery({ queryKey: ['user', '1'], queryFn: () => fetchUser('1'), }); // 第二个查询,依赖于第一个查询的结果 const { data: posts } = useQuery({ queryKey: ['posts', user?.id], queryFn: () => fetchUserPosts(user.id), // 只有当user存在时才执行查询 enabled: !!user, }); // ... } 

9.4 处理查询错误

问题:查询错误时,用户体验不佳。

解决方案:

const { data, error, isError } = useQuery({ queryKey: ['data'], queryFn: fetchData, // 自定义错误处理 onError: (error) => { console.error('Query error:', error); // 可以在这里显示错误通知 }, // 重试配置 retry: (failureCount, error) => { if (error.status === 404) return false; return failureCount < 3; }, }); if (isError) { return <div>Error: {error.message}</div>; } 

9.5 处理查询取消

问题:组件卸载时,未完成的查询仍然在继续。

解决方案:

const { data } = useQuery({ queryKey: ['data'], queryFn: async ({ signal }) => { const response = await fetch('/api/data', { signal }); return response.json(); }, }); 

9.6 处理SSR和CSR数据不一致

问题:在Next.js中,服务端渲染的数据与客户端获取的数据不一致。

解决方案:

function Component({ initialData }) { const { data } = useQuery({ queryKey: ['data'], queryFn: fetchData, // 使用服务端提供的初始数据 initialData, // 配置何时重新获取数据 refetchOnMount: true, staleTime: 1000 * 60, // 1分钟 }); // ... } 

10. 总结

Next.js和React Query的结合为前端数据获取和缓存管理提供了强大的解决方案。通过Next.js的服务端渲染能力,我们可以实现快速的首屏加载和良好的SEO;通过React Query的客户端数据管理,我们可以实现高效的数据获取、缓存和同步。

在实际应用中,我们可以根据具体需求选择合适的数据获取策略,如服务端预取、客户端获取、并行查询、依赖查询等。同时,通过合理配置缓存策略、重试机制和重新获取条件,我们可以优化应用性能,提供更好的用户体验。

通过本文介绍的技术和最佳实践,开发者可以充分利用Next.js和React Query的优势,构建高性能、响应灵敏的现代Web应用。