在之前的文章中,我们完成了使用shopify store api 构建一个简单商店的项目,但是在这个项目中,我们在商品详情页面中使用的是useEffect来查询获取数据,我们只是简单的发送请求,并没有处理错误和等待的情况,如果在useEffect中处理错误和请求状态,会麻烦一些,所以在本文中我们来优化一下,在Next.js应用程序中设置和使用 React Query来发送请求并处理错误或者请求状态。如果你没有按照之前的文章进行,可以重新创建一个nextjs项目npx create-next-app@latest nextjs14-react-query
,但是需要自己配置一下你的mock api接口.
一、整理文件结构
在开始之前,我们先整理一下目录,为了遵循 Next.js 13 和 14 的目录建议,所有页面和相关文件应放置在 src/app 目录内。这种方式不仅使代码结构更加清晰,还能充分利用 Next.js 提供的新功能和优化。所以将pages里面的product和lib文件夹及文件全部移动到app/目录下面。把[handle].tsx 文件修改为[handle]/pages.tsx 文件。 修改完成后的目录类似这样。
二、安装 React Query
现在我们来安装本文中需要的 React Query 依赖项了。打开终端并运行以下命令来安装它们:
shell
npm i @tanstack/react-query
npm i -D @tanstack/eslint-plugin-query
npm i -D @tanstack/react-query-devtools
你也可以使用yarn 或者pnpm安装。
- @tanstack/react-query-- 此包是 React Query 的 React 绑定库。它提供hook和组件以将 React Query 与 React 应用程序集成。
- @tanstack/eslint-plugin-query-- 这是专为 React Query 设计的 ESLint 插件。它提供了 ESLint 规则,以在代码中使用 React Query 时强制执行最佳实践和约定。
- @tanstack/react-query-devtools-- 此软件包提供了一组用于在应用程序中调试和检查 React Query 的开发工具。
三、创建 React Query客户端Proverider
因为QueryClientProvider在底层是使用useContext,只能在客户端组件中使用。在以前的 Next.js 版本中,尤其是 v13 以下的版本,通常将QueryClientProvider包装在文件中的根组件_app.tsx周围,将其视为客户端组件。但是,由于 Next.js 13 以后的版本中的所有组件现在都是服务器组件。所以不能直接嵌入到layout.tsx文件中。因此,需要先创建一个客户端组件。
在app下面创建一个providers
目录,然后在里面创建一个ReactQueryProvider.tsx
文件,添加下面的代码
tsx
'use client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { useState } from 'react';
function ReactQueryProvider({ children }: React.PropsWithChildren) {
const [queryClient] = useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
// 默认设置
// 设置0以上,以避免在客户端立即重新读取
staleTime: 60 * 1000,
},
},
})
)
return (
<QueryClientProvider client={client}>
{children}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}
export default ReactQueryProvider;
在代码的顶部,我们声明了这个是客户端组件,这个组件接受一个children属性,代表要包裹的子组件。在组件内部,使用 useState 钩子创建一个新的 QueryClient 实例,然后配置了一下默认配置。同时配置了ReactQueryDevtools调试工具。
三、将 ReactQueryProvider包装在根节点
打开app/layout.tsx
文件,将ReactQueryProvider组件包装在{children}外面.
tsx
import type { Metadata } from "next";
import { Inter } from "next/font/google";
// import './globals.css';
import ReactQueryProvider from "@/providers/ReactQueryProvider";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body className={inter.className}>
<ReactQueryProvider>
{children}
</ReactQueryProvider>
</body>
</html>
);
}
通过在根节点处渲染提供程序,整个应用中的所有其他客户端组件都将能够访问查询客户端。需要要注意一点,ReactQueryProvider仅包装{children}
而不是整个<html>
文档,这使 Next.js 更容易优化服务器组件的静态部分。
四、在页面中使用react query
现在来到我们的商品详情页app/product/[handle]/page.tsx
, 先注释掉useEffect 和useState的相关代码。
tsx
//注释掉这些代码
// const [singleProduct, setSingleProduct] = useState<ISingleProduct | undefined>(undefined);
// useEffect(() => {
// const fetchData = async () => {
// if (params?.handle) {
// try {
// const response = await fetchSingleProduct(params.handle);
// const singleProduct = response.body;
// setSingleProduct(singleProduct);
// } catch (error) {
// console.error('Error fetching product:', error);
// }
// }
// };
// fetchData();
// }, [params?.handle]);
然后替换为使用useQuery 查询
tsx
const { isLoading, error, data: singleProduct } = useQuery({
queryKey: ['singleProduct', params?.handle],
queryFn: async () => {
const response = await fetchSingleProduct(params.handle);
if (response.status !== 200) {
throw new Error(response.error || 'Failed to fetch product');
}
return response.body; // 只返回 body 部分
},
enabled: !!params?.handle, // 只有在 handle 存在时才执行查询
});
因为我们的的fetchSingleProduct
函数返回的数据类型是这样的
ts
export type ISingleProductResponst = {
status: number;
body?: ISingleProduct;
error?: string;
};
response.body 才是我们的需要的商品详情数据,所以返回 return response.body;
,然后判断 isLoading
和error
状态, 返回不同的样式。
tsx
if (isLoading) {
return <div>Loading...</div>;
}
if (error) {
return <div>Error fetching product: {(error as Error).message}</div>;
}
现在打开页面,随便点击一个商品,查看页面是否正常。实际效果是有一个短暂的加载状态,但是样式不太好看。下面稍微修改一下样式。
五、 测试loading
创建一个文件src/app/product/[handle]/loading.tsx
文件,里面添加这些代码
tsx
import React from 'react'
const Loading = () => {
return (
<div className="flex items-center justify-center w-full h-[100vh] text-gray-900 dark:text-gray-100 dark:bg-gray-950">
<div>
<h1 className="text-xl md:text-2xl font-bold flex items-center">L<svg stroke="currentColor" fill="currentColor" stroke-width="0"
viewBox="0 0 24 24" className="animate-spin" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg">
<path
d="M12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2ZM13.6695 15.9999H10.3295L8.95053 17.8969L9.5044 19.6031C10.2897 19.8607 11.1286 20 12 20C12.8714 20 13.7103 19.8607 14.4956 19.6031L15.0485 17.8969L13.6695 15.9999ZM5.29354 10.8719L4.00222 11.8095L4 12C4 13.7297 4.54894 15.3312 5.4821 16.6397L7.39254 16.6399L8.71453 14.8199L7.68654 11.6499L5.29354 10.8719ZM18.7055 10.8719L16.3125 11.6499L15.2845 14.8199L16.6065 16.6399L18.5179 16.6397C19.4511 15.3312 20 13.7297 20 12L19.997 11.81L18.7055 10.8719ZM12 9.536L9.656 11.238L10.552 14H13.447L14.343 11.238L12 9.536ZM14.2914 4.33299L12.9995 5.27293V7.78993L15.6935 9.74693L17.9325 9.01993L18.4867 7.3168C17.467 5.90685 15.9988 4.84254 14.2914 4.33299ZM9.70757 4.33329C8.00021 4.84307 6.53216 5.90762 5.51261 7.31778L6.06653 9.01993L8.30554 9.74693L10.9995 7.78993V5.27293L9.70757 4.33329Z">
</path>
</svg> ading . . .</h1>
</div>
</div>
)
}
export default Loading
在src/app/product/[handle]/page.tsx
文件导入Loading组件后,修改loading相关代码。
tsx
if (isLoading) {
return <Loading/>;
}
添加一个延迟,来测试一下
ts
// 一个简单的延迟函数
const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
tsx
queryFn: async () => {
await delay(3000); // 添加3秒的延迟
...
会看到一个漂亮的加载动画。
六、测试错误状态
然后测试一下error状态,在queryFn中故意抛出一个错误来测试 error 状态。
tsx
queryFn: async () => {
await delay(3000); // 添加3秒的延迟
// 模拟错误
throw new Error('Simulated error for testing');
...
同样添加一个src/app/product/[handle]/error.tsx
文件,在tailwindui中找一个样式,因为使用的样式需要使用headlessui的Dialog及相关组件,所以安装一下headlessui
shell
npm install @headlessui/react@latest
图标这里就不安装了,简单的删除掉图标相关代码。
tsx
<Dialog open={open} onClose={setOpen} className="relative z-10">
<DialogBackdrop
transition
className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity data-[closed]:opacity-0 data-[enter]:duration-300 data-[leave]:duration-200 data-[enter]:ease-out data-[leave]:ease-in"
/>
<div className="fixed inset-0 z-10 w-screen overflow-y-auto">
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
<DialogPanel
transition
className="relative transform overflow-hidden rounded-lg bg-white text-left shadow-xl transition-all data-[closed]:translate-y-4 data-[closed]:opacity-0 data-[enter]:duration-300 data-[leave]:duration-200 data-[enter]:ease-out data-[leave]:ease-in sm:my-8 sm:w-full sm:max-w-lg data-[closed]:sm:translate-y-0 data-[closed]:sm:scale-95"
>
<div className="bg-white px-4 pb-4 pt-5 sm:p-6 sm:pb-4">
<div className="sm:flex sm:items-start">
<div className="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
</div>
<div className="mt-3 text-center sm:ml-4 sm:mt-0 sm:text-left">
<DialogTitle as="h3" className="text-base font-semibold leading-6 text-gray-900">
请求错误
</DialogTitle>
<div className="mt-2">
<p className="text-sm text-gray-500">
Are you sure you want to deactivate your account? All of your data will be permanently removed.
This action cannot be undone.
</p>
</div>
</div>
</div>
</div>
<div className="bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6">
<button
type="button"
onClick={() => setOpen(false)}
className="inline-flex w-full justify-center rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500 sm:ml-3 sm:w-auto"
>
确定
</button>
</div>
</DialogPanel>
</div>
</div>
</Dialog>
刷新页面后,等加载完成后会显示如下样式
说明: 本文只是简单演示erro样式,在实际项目中可能要进行相应的后续处理,不在本文的讨论范围内了。
完整请求状态loading 和error 如下:
删除测试代码,这样我们就完成了在商品详情页使用react query 请求并处理loading 和错误了。