Nextjs14中使用react Query查询

之前的文章中,我们完成了使用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;,然后判断 isLoadingerror 状态, 返回不同的样式。

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 和错误了。

原文地址

相关推荐
用户2181697049301 小时前
Flutter(四)Dart语法 空安全 运算符 流程控制
前端
许彰午1 小时前
政务督办的分合模式:主办协办的并发审批
前端·javascript·政务
用户69371750013841 小时前
AI时代,程序员该往哪走?
前端·后端
ttwuai1 小时前
GoFrame 后台日志清空失败:无 WHERE 删除为什么被拦住
前端·golang
易筋紫容1 小时前
创建型模式:对象的诞生艺术
开发语言·前端·javascript
半句唐诗2 小时前
我是如何通过 Access Token 成功发布第一个 npm 包的
前端·npm·node.js
程序员黑豆2 小时前
鸿蒙应用开发:@Provider 与 @Consumer 跨组件双向同步详解
前端·harmonyos
paopaokaka_luck2 小时前
基于springboot3+vue3的智能文库平台(AI智能搜索、AI智能汇总、实时在线状态展示、多格式文档预览与富文本编辑、Echarts图形化分析)
前端·网络·spring boot·网络协议·echarts
牧艺2 小时前
cos-design PhotoAlbum:用 CSS 3D 做一个「能翻页」的实体相册
前端·css·交互设计
洪贺2 小时前
从原始采样到可缩放心电图:用 h5ECG 绘制自己的 ECG
前端