Featured image of post Nextjs使用react Query并配置预取数据详细教程

Nextjs使用react Query并配置预取数据详细教程

Nextjs使用react Query详细教程

阅读时长: 8 分钟
共 3999字
作者: eimoon.com

React Query 是一个强大的 React 应用数据获取和缓存库。它简化了从各种来源(如 API、数据库或无服务器函数)获取和管理数据的过程。通过利用 React Query,您可以优化应用程序的数据获取、减少不必要的网络请求并处理缓存。下面我们一起来学习如何在 Next.js 14 中使用 React Query 并配置预取数据。

一、初始化一个nextjs14项目

首先,让我们启动一个新的 Next.js 14 应用程序。导航到您的工作目录。在那里打开一个新终端,并根据您喜欢的包管理器执行以下命令以开始项目脚手架过程。

npx create-next-app@latest nextjs14-react-query

在接下来的询问环节中,选择以下选项:

✔ Would you like to use ESLint? … **No** / Yes
✔ Would you like to use Tailwind CSS? … No / **Yes**
✔ Would you like to use `src/` directory? … No / **Yes**
✔ Would you like to use App Router? (recommended) … No / **Yes**
✔ Would you like to customize the default import alias? … **No** / Yes

安装完成后,您可以在VS Code中打开该项目。

二、在 Next.js 中设置 React Query

1.安装软件包

要在Next.js应用程序中开始使用 React Query,首先需要安装该软件包。React Query包原来叫React Query ,后来修改了名称TanStack Query,包括了更多框架, 所以我们要安装@tanstack/react-query。注意不要安装多个版本(react-query | @tanstack/react-query),否则可能会引起不必要的麻烦。 通过 npm 安装 React Query, 安装命令如下:

npm i @tanstack/react-query

2.安装ESLint 插件

为了获得更顺畅的开发体验并尽早发现错误和不一致之处,建议使用 ESLint 插件查询。您可以使用以下 npm 命令安装它:

npm i -D @tanstack/eslint-plugin-query

3.安装开发调试包

为了方便调试,还建议安装react-query-devtools包, 此软件包提供了一组用于在应用程序中调试和检查 React Query 的开发工具。

npm i -D @tanstack/react-query-devtools

三、创建 React Query客户端提供程序

为了在 Next.js 应用中使用 React Query,我们需要创建一个全局数据共享的上下文(Context)。通常,我们会将这个上下文提供程序包裹在应用的根布局组件上,这样整个应用的所有组件都可以方便地访问和使用 React Query 提供的数据获取和缓存功能。
在 Next.js 13 及之后的版本中,由于引入了服务器组件的概念,而 React Query 并不适用于服务器端渲染,所以我们需要采用一些特殊的方式来确保 QueryClientProvider 只在客户端组件中被渲染

1.创建一个QueryClient 提供程序

创建一个文件 src/app/provider/ReactQueryProvider.tsx,里面添加这些代码:

'use client'

import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { useState } from 'react'

export const ReactQueryClientProvider = ({ children }: { children: React.ReactNode }) => {
  const [queryClient] = useState(
    () =>
      new QueryClient({
        defaultOptions: {
          queries: {
            // With SSR, we usually want to set some default staleTime
            // above 0 to avoid refetching immediately on the client
            staleTime: 60 * 1000,
          },
        },
      })
  )
  return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
}

在代码的顶部,我们声明了这个是客户端组件,这个组件接受一个children属性,代表要包裹的子组件。在组件内部,使用 useState 钩子创建一个新的 QueryClient 实例,然后配置了默认配置。同时配置了ReactQueryDevtools调试工具。

2、将 QueryClient 提供程序包装在根节点周围

现在,是时候将查询客户端提供程序包装在节点children周围,以确保组件树中的所有组件都可以访问查询客户端。在nextjs14中,这个文件是layout.tsx.打开文件app/layout.tsxReactQueryProvider组件包装在{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 更容易优化服务器组件的静态部分。

四、创建一个后端测试接口

我们先来创建一个请求函数。创建一个文件src/app/lib/api-request.ts,添加下面内容:

export type User = {
  id: number;
  name: string;
  email: string;
};

export async function getUsers() {
  const res = await fetch('https://jsonplaceholder.typicode.com/users');
  const users = (await res.json()) as User[];
  return users;
}

这里后端使用https://jsonplaceholder.typicode.com/ 提供的免费测试端口。

五、使用react query

1.使用react query

创建一个页面src/app/users/page.tsx,添加下面代码

'use client';

import { User, getUsers } from '@/app/utils/api-requests';
import { useQuery } from '@tanstack/react-query';
import Image from 'next/image';
import React from 'react';

export default function ListUsers() {

  const { data } = useQuery({
    queryKey: ['list-user'],
    queryFn: () => getUsers(),
    staleTime: 5 * 1000,
  });

  return (
    <main style={{ maxWidth: 1200, marginInline: 'auto', padding: 20 }}>
      {
        <div
          style={{
            display: 'grid',
            gridTemplateColumns: '1fr 1fr 1fr 1fr',
            gap: 20,
          }}
        >
          {data?.map((user) => (
            <div
              key={user.id}
              style={{ border: '1px solid #ccc', textAlign: 'center' }}
            >
              <Image
                src={`https://robohash.org/${user.id}?set=set2&size=180x180`}
                alt={user.name}
                width={180}
                height={180}
              />
              <h3>{user.name}</h3>
            </div>
          ))}
        </div>
      }
    </main>
  );
}

2.配置next.config.js使用外部图像

因为图片使用了外部图片,所以需要配置一下nextjs以允许外部图像。

const nextConfig = {
 images: {
   remotePatterns: [
     {
       protocol: 'https',
       hostname: 'robohash.org',
     },
   ],
 },
};

module.exports = nextConfig;

现在打开浏览器,访问http://localhost:3000/users,应该能看到下面的页面

alt text

六、使用 isLoading 和 isError 管理查询状态

React Query 提供了返回变量isLoading和error,您可以使用它们来管理加载状态并优雅地处理组件中的错误。

1.创建一个loading组件

创建一个src/app/users/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" strokeWidth="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

2.创建一个error组件

因为要使用headlessui中一些组件,所以安装一下headlessui

 npm install @headlessui/react@latest

创建一个src/app/users/error.tsx 组件,添加下面代码:

"use client"
import { useState } from 'react'
import { Dialog, DialogBackdrop, DialogPanel, DialogTitle } from '@headlessui/react'

interface ErrorProps {
    message: string;
  }  

export default function ErrorPage({ message }: ErrorProps) {
  const [open, setOpen] = useState(true)

  return (
    <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>
  )
}

然后修改page.tsx 中代码:


  const { isLoading,error,data } = useQuery({
    queryKey: ['list-user'],
    queryFn: async () => {
      throw new Error('测试模拟错误');//故意抛出错误
      const response = await getUsers();
      return response; 
   },
    staleTime: 5 * 1000,
  });

  if (isLoading) {
    return <Loading/>;
  }

  if (error) {
    return <ErrorPage message={(error as Error).message}/>;
  }

在这段代码中,为了测试错误装置,我们在queryFn 中故意抛出了一个错误,然后打开http://localhost:3000/users 页面,可以看到一个短暂的加载状态后 alt text
弹出错误提醒 alt text

测试完成后,删除故意抛出错误的代码。

七、从服务器预取数据

React Query 内置了对服务器预取数据的支持,并将其与服务器组件一起发送。我们的数据与要呈现的标记一起发送,从而缩短了等待加载的时间,从而带来了更好的用户体验。而且,如果我们的服务器数据过时了,React Query 会自动在客户端重新获取查询。 这种方法的好处:

  • SEO 优化:服务器渲染期间预取数据有助于在初始 HTML 响应中包含必要数据,从而实现 SEO 优化。
  • 最佳性能:在客户端对查询客户端状态进行脱水和再水化可减少冗余数据提取并提高性能。

下面我们来实现一下。

1.第一种方法设置查询初始数据

我们在一个新页面initial-data中来实现,创建一个文件夹src/app/initial-data,里面添加一个list-user.tsx文件:


'use client';

import { User, getUsers } from '../utils/api-requests';
import { useQuery } from '@tanstack/react-query';
import Image from 'next/image';
import React from 'react';

export default function ListUsers({ users }: { users: User[] }) {

  const { data } = useQuery({
    queryKey: ['initial-users'],
    queryFn: () => getUsers(),
    initialData: users,
    staleTime: 5 * 1000,
  });

  return (
    <main style={{ maxWidth: 1200, marginInline: 'auto', padding: 20 }}>
      {
        <div
          style={{
            display: 'grid',
            gridTemplateColumns: '1fr 1fr 1fr 1fr',
            gap: 20,
          }}
        >
          {data.map((user) => (
            <div
              key={user.id}
              style={{ border: '1px solid #ccc', textAlign: 'center' }}
            >
              <Image
                src={`https://robohash.org/${user.id}?set=set2&size=180x180`}
                alt={user.name}
                width={180}
                height={180}
              />
              <h3>{user.name}</h3>
            </div>
          ))}
        </div>
      }
    </main>
  );
}

里面的代码和 user 页面的基本一样,只是在useQuery中添加了initialData初始数据,并添加了一个props。

src/app/initial-data创建一个page.tsx页面,添加下面内容:

import { getUsers } from '../utils/api-requests';
import ListUsers from './list-users';

export default async function InitialData() {
  const users = await getUsers();
  return <ListUsers users={users} />;
}

我们从父组件中查询users数据,然后传递给ListUsers 子组件,在子组件中,设置为初始数据为users。 查看浏览器页面http://localhost:3000/initial-data,打开开发折工具查看网络 可以类似页面 alt text alt text

虽然这种方法简单直接,但并不是react query 推荐的。下面我们使用更优的方法来实现数据预取。

2.第二种方法 使用 Hydration 和 Dehydration 预取数据

在 React Query 和 Next.js 中使用 hydration 和 dehydration 预取数据是一种优化服务器渲染的 React 应用程序中数据提取和渲染的技术。也是React Query推荐使用的。

重命名src/app/user/page.tsxsrc/app/user/list-user.tsx

'use client';

import { getUsers } from '../utils/api-requests';
import { useQuery } from '@tanstack/react-query';
import Image from 'next/image';
import React from 'react';

export default function ListUsers() {

  const { data } = useQuery({
    queryKey: ['list-users'],
    queryFn: () => getUsers(),
    staleTime: 10 * 1000,
  });

  return (
    <main style={{ maxWidth: 1200, marginInline: 'auto', padding: 20 }}>
       {
        <div
          style={{
            display: 'grid',
            gridTemplateColumns: '1fr 1fr 1fr 1fr',
            gap: 20,
          }}
        >
          {data?.map((user) => (
            <div
              key={user.id}
              style={{ border: '1px solid #ccc', textAlign: 'center' }}
            >
              <Image
                src={`https://robohash.org/${user.id}?set=set2&size=180x180`}
                alt={user.name}
                width={180}
                height={180}
              />
              <h3>{user.name}</h3>
            </div>
          ))}
        </div>
      }
    </main>
  );
}

然后创建一个新的page.tsx 文件,添加下面的代码:

import { dehydrate } from "@tanstack/query-core";
import ListUsers from "@/app/users/list-user";
import { HydrationBoundary, QueryClient } from "@tanstack/react-query";
import { getUsers } from "@/app/utils/api-requests";

export default async function Users() {
  const queryClient = new QueryClient(); 

  await queryClient.prefetchQuery({
    queryKey: ["hydrate-users"],
    queryFn: getUsers,
  });

  return (
    <HydrationBoundary state={dehydrate(queryClient)}>
      <ListUsers />
    </HydrationBoundary>
  );
}

在这段代码中:

  • 首先创建了一个 React Query 的查询客户端实例。

  • await queryClient.prefetchQuery({…});: 这里使用了 prefetchQuery 方法来预取数据。这个方法接受一个对象作为参数,其中包含:

    • queryKey: 查询的唯一标识符,在这里是 “list-user”;
    • queryFn: 一个异步函数,用于获取实际的数据。在这里是 getUsers 函数。
  • dehydrate(queryClient): 这个方法将查询客户端的状态"脱水",以便在客户端重新"水合"(hydrate)

  • 使用 <HydrationBoundary state={dehydrate(queryClient)}>包裹您的树。 打开浏览器访问开发这工具或者react-query-devtools调试工具,应该类似这样: alt text

总结

在本文中,我们完成了如何在 Next.js 应用程序中设置和使用 React Query。我们还完成了如何在服务器上整合查询并在客户端上使用它们。希望本文对您有用。如果您有任何问题或反馈,请随时在下面的评论部分分享。 感谢您的阅读并祝您编码愉快!

微信公众号

使用 Hugo 构建
主题 StackJimmy 设计