二、安装 React Query
现在我们来安装本文中须要的 React Query 依赖项了。打开终端并运行以下命令来安装它们:
npm i @tanstack/react-querynpm i -D @tanstack/eslint-plugin-querynpm i -D @tanstack/react-query-devtools
你也可以利用yarn或者pnpm安装。
由于QueryClientProvider在底层是利用useContext,只能在客户端组件中利用。在以前的 Next.js 版本中,尤其是 v13 以下的版本,常日将QueryClientProvider包装在文件中的根组件_app.tsx周围,将其视为客户端组件。但是,由于 Next.js 13 往后的版本中的所有组件现在都是做事器组件。以是不能直接嵌入到layout.tsx文件中。因此,须要先创建一个客户端组件。

在app下面创建一个providers目录,然后在里面创建一个ReactQueryProvider.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}表面.
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的干系代码。
//注释掉这些代码 // 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 查询
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 函数返回的数据类型是这样的
export type ISingleProductResponst = { status: number; body?: ISingleProduct; error?: string;};
response.body 才是我们的须要的商品详情数据,以是返回 return response.body;,然后判断 isLoading 和error 状态, 返回不同的样式。
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文件,里面添加这些代码
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干系代码。
if (isLoading) { return <Loading/>; }
添加一个延迟,来测试一下
// 一个大略的延迟函数const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
queryFn: async () => { await delay(3000); // 添加3秒的延迟 ...
会看到一个俊秀的加载动画。
六、测试缺点状态
然后测试一下error状态,在queryFn中故意抛出一个缺点来测试 error 状态。
queryFn: async () => { await delay(3000); // 添加3秒的延迟 // 仿照缺点 throw new Error('Simulated error for testing'); ...
同样添加一个src/app/product/[handle]/error.tsx 文件,在tailwindui中找一个样式,由于利用的样式须要利用headlessui的Dialog及干系组件,以是安装一下headlessui
npm install @headlessui/react@latest
图标这里就不安装了,大略的删除掉图标干系代码。
<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 和缺点了。