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

原文地址

相关推荐
麒麟而非淇淋38 分钟前
AJAX 入门 day1
前端·javascript·ajax
2401_8581205340 分钟前
深入理解MATLAB中的事件处理机制
前端·javascript·matlab
阿树梢44 分钟前
【Vue】VueRouter路由
前端·javascript·vue.js
随笔写2 小时前
vue使用关于speak-tss插件的详细介绍
前端·javascript·vue.js
史努比.2 小时前
redis群集三种模式:主从复制、哨兵、集群
前端·bootstrap·html
快乐牌刀片883 小时前
web - JavaScript
开发语言·前端·javascript
miao_zz3 小时前
基于HTML5的下拉刷新效果
前端·html·html5
Zd083 小时前
14.其他流(下篇)
java·前端·数据库
藤原拓远3 小时前
JAVAWeb-XML-Tomcat(纯小白下载安装调试教程)-HTTP
前端·firefox
重生之我在20年代敲代码3 小时前
HTML讲解(一)body部分
服务器·前端·html