在上一篇文章中,你可以使用流式传输技术改进了仪表板的初始化加载性能。现在让我们转到页面/invoices,了解如何添加搜索和分页。
在本章节中,我们将要探讨两个内容:
了解如何使用 Next.js API useSearchParams:、usePathname和useRouter。
使用 URL 搜索参数实现搜索和分页。
开始代码
编辑 /dashboard/invoices/page.tsx
文件,实现查询功能
import Pagination from '@/app/ui/invoices/pagination';
import Search from '@/app/ui/search';
import Table from '@/app/ui/invoices/table';
import { CreateInvoice } from '@/app/ui/invoices/buttons';
import { lusitana } from '@/app/ui/fonts';
import { InvoicesTableSkeleton } from '@/app/ui/skeletons';
import { Suspense } from 'react';
export default async function Page() {
return (
<div className="w-full">
<div className="flex w-full items-center justify-between">
<h1 className={`${lusitana.className} text-2xl`}>Invoices</h1>
</div>
<div className="mt-4 flex items-center justify-between gap-2 md:mt-8">
<Search placeholder="Search invoices..." />
<CreateInvoice />
</div>
{/* <Suspense key={query + currentPage} fallback={<InvoicesTableSkeleton />}>
<Table query={query} currentPage={currentPage} />
</Suspense> */}
<div className="mt-5 flex w-full justify-center">
{/* <Pagination totalPages={totalPages} /> */}
</div>
</div>
);
}
当然,这样粘贴过去是会报错的,因为我们还没有实现分页、搜索、发票表格的功能。
我们先把报错的先注释掉,然后一个一个功能组件的实现吧。
花一些时间熟悉该页面和您将要使用的组件:
<Search/>
允许用户搜索特定的发票。<Pagination/>
允许用户在发票页面之间导航。<Table/>
显示发票。
您的搜索功能将跨越客户端和服务器。当用户在客户端上搜索发票时,URL参数将被更新,数据将被提取到服务器上,表格将使用新数据在服务器上重新呈现。
为什么使用 URL 搜索参数?
如上所述,您将使用 URL搜索参数来管理搜索状态。如果您习惯使用客户端状态,这种模式可能很新奇。
使用 URL 参数实现搜索有两个好处:
可收藏和共享的 URL:由于搜索参数位于URL中,因此用户可以收藏应用程序的当前状态(包括他们的搜索查询和过滤器),以供将来参考或共享。
服务器端渲染和初始加载:URL参数可以在服务器上直接使用来渲染初始状态,从而更容易处理服务器渲染。
分析和跟踪:直接在URL中使用搜索查询和过滤器可以更轻松地跟踪用户行为,而无需额外的客户端逻辑。
添加搜索功能
这些是您将用来实现搜索功能的 Next.js 客户端挂钩:
useSearchParams
允许您访问当前URL的参数。例如,此URL的搜索参数/dashboard/invoices?page=1&query=pending如下所示:{page: '1', query: 'pending'}。usePathname
让您读取当前URL的路径名。例如,对于路线/dashboard/invoices,usePathname将返回'/dashboard/invoices'。useRouter
以编程方式启用客户端组件内路由之间的导航。您可以使用多种方法。
以下是实施步骤的简要概述:
捕获用户的输入。
使用搜索参数更新 URL。
保持 URL 与输入字段同步。
更新表格以反映搜索查询。
捕获用户输入
编辑<Search>
组件(/app/ui/search.tsx),创建search.tsx
"use client";
import { MagnifyingGlassIcon } from "@heroicons/react/24/outline";
export default function Search({ placeholder }: { placeholder: string }) {
function handleSearch(term: string) {
console.log(term);
}
return (
<div className="relative flex flex-1 flex-shrink-0">
<label htmlFor="search" className="sr-only">
搜索
</label>
<input
className="peer block w-full rounded-md border border-gray-200 py-[9px] pl-10 text-sm outline-2 placeholder:text-gray-500"
placeholder={placeholder}
onChange={(e) => {
handleSearch(e.target.value);
}}
/>
<MagnifyingGlassIcon className="absolute left-3 top-1/2 h-[18px] w-[18px] -translate-y-1/2 text-gray-500 peer-focus:text-gray-900" />
</div>
);
}
然后我们把page.tsx的注释内容修改一下,把搜索功能取消注释
访问地址:http://172.16.100.104/dashboard/invoices 即可看到我们刚刚的修改
通过在开发者工具中打开控制台来测试它是否正常工作,然后在搜索字段中输入。您应该看到搜索词记录到控制台。太棒了!您捕获了用户的搜索输入。现在,您需要使用搜索词更新 URL。
使用搜索参数更新 URL
编辑 /app/ui/search.tsx 修改部分内容
use SearchParams
从导入钩子'next/navigation'
,并将其分配给变量:
'use client';
import { MagnifyingGlassIcon } from '@heroicons/react/24/outline';
import { useSearchParams } from 'next/navigation';
export default function Search() {
const searchParams = useSearchParams();
function handleSearch(term: string) {
console.log(term);
}
// ...
}
在里面handleSearch
,创建一个新的URLSearchParams
使用新searchParams
变量的实例。
'use client';
import { MagnifyingGlassIcon } from '@heroicons/react/24/outline';
import { useSearchParams } from 'next/navigation';
export default function Search() {
const searchParams = useSearchParams();
function handleSearch(term: string) {
const params = new URLSearchParams(searchParams);
}
// ...
}
URLSearchParams
是一个WebAPI,它提供用于操作URL查询参数的实用方法。您无需创建复杂的字符串文字,而是可以使用它来获取参数字符串,例如?page=1&query=a。
接下来,set
根据用户的输入获取params
字符串。如果输入为空,则需要delete
:
'use client';
import { MagnifyingGlassIcon } from '@heroicons/react/24/outline';
import { useSearchParams } from 'next/navigation';
export default function Search() {
const searchParams = useSearchParams();
function handleSearch(term: string) {
const params = new URLSearchParams(searchParams);
if (term) {
params.set('query', term);
} else {
params.delete('query');
}
}
// ...
}
在页面使用F12可以看到调试的信息
现在您有了查询字符串。您可以使用Next.jsuseRouter
和usePathname
钩子来更新 URL。导入useRouter
并使用内部的方法:usePathname
'next/navigation'
replace
use Router()
handleSearch
。
'use client';
import { MagnifyingGlassIcon } from '@heroicons/react/24/outline';
import { useSearchParams, usePathname, useRouter } from 'next/navigation';
export default function Search() {
const searchParams = useSearchParams();
const pathname = usePathname();
const { replace } = useRouter();
function handleSearch(term: string) {
const params = new URLSearchParams(searchParams);
if (term) {
params.set('query', term);
} else {
params.delete('query');
}
replace(`${pathname}?${params.toString()}`);
}
}
以下是具体情况:
${pathname}
是当前路径,就您而言,"/dashboard/invoices"
。当用户在搜索栏中输入内容时,params.toString()将此输入转换为 URL 友好的格式。replace(${pathname}?${params.toString()})
使用用户的搜索数据更新URL。例如,/dashboard/invoices?query=lee如果用户搜索“Lee”。得益于
Next.js
的客户端导航(您在页面间导航章节中了解过),无需重新加载页面即可更新 URL。
保持 URL 和输入同步
为了确保输入字段与URL同步并在共享时填充,您可以defaultValue
通过读取以下内容将输入传递给输入searchParams
:
编辑 /app/ui/search.tsx 修改部分内容
<input
className="peer block w-full rounded-md border border-gray-200 py-[9px] pl-10 text-sm outline-2 placeholder:text-gray-500"
placeholder={placeholder}
onChange={(e) => {
handleSearch(e.target.value);
}}
defaultValue={searchParams.get('query')?.toString()}
/>
关于 React 中受控组件和非受控组件的区别,以及 defaultValue 和 value 属性的使用。让我来详细解释一下:
受控组件 vs 非受控组件
受控组件:组件的状态完全由 React 控制。通常使用 value 属性和 onChange 事件来管理。
非受控组件:组件的状态由 DOM 自身管理。通常使用 defaultValue 属性来设置初始值
value vs defaultValue
value:用于受控组件,需要配合 onChange 使用,每次输入都会触发状态更新。
defaultValue:用于非受控组件,只设置初始值,之后的输入不会触发 React 状态更新。
这里使用了 defaultValue,表明这是一个非受控组件。初始值从URL的查询参数中获取,但之后的输入不会直接更新 React 的状态。
为什么这样做?
性能考虑:非受控组件可能在某些情况下性能更好,因为它不需要 React 为每次输入更新状态。
URL 状态管理:您将搜索查询保存在 URL 中,而不是组件状态中。这允许用户分享或书签特定的搜索结果。
onChange 的作用
尽管这是一个非受控组件,但仍然使用了 onChange 事件。这里 handleSearch 函数可能用于更新 URL 或触发搜索,而不是更新组件的内部状态。
这种方法结合了非受控组件的简单性和URL状态管理的优势。输入框的值由浏览器管理,而不是 React 状态。搜索逻辑和 URL 更新通过 onChange 事件处理。
更新表格
最后,您需要更新表格组件以反映搜索查询,返回发票页面。页面组件接受一个名为的 prop searchParams,因此您可以将当前URL参数传递给该组件。
编辑 /app/dashboard/invoices/page.tsx
文件
import Pagination from '@/app/ui/invoices/pagination';
import Search from '@/app/ui/search';
import Table from '@/app/ui/invoices/table';
import { CreateInvoice } from '@/app/ui/invoices/buttons';
import { lusitana } from '@/app/ui/fonts';
import { Suspense } from 'react';
import { InvoicesTableSkeleton } from '@/app/ui/skeletons';
export default async function Page({
searchParams,
}: {
searchParams?: {
query?: string;
page?: string;
};
}) {
const query = searchParams?.query || '';
const currentPage = Number(searchParams?.page) || 1;
return (
<div className="w-full">
<div className="flex w-full items-center justify-between">
<h1 className={`${lusitana.className} text-2xl`}>Invoices</h1>
</div>
<div className="mt-4 flex items-center justify-between gap-2 md:mt-8">
<Search placeholder="Search invoices..." />
<CreateInvoice />
</div>
<Suspense key={query + currentPage} fallback={<InvoicesTableSkeleton />}>
<Table query={query} currentPage={currentPage} />
</Suspense>
<div className="mt-5 flex w-full justify-center">
{/* <Pagination totalPages={totalPages} /> */}
</div>
</div>
);
}
这些需要简单实现Table表格的逻辑用于验证查询是否成功,创建table页面编辑/app/ui/invoices/table.tsx
import Image from "next/image";
import { formatDateToLocal, formatCurrency } from "@/app/lib/utils";
import { fetchFilteredInvoices } from "@/app/lib/data";
export default async function InvoicesTable({
query,
currentPage,
}: {
query: string;
currentPage: number;
}) {
const invoices = await fetchFilteredInvoices(query, currentPage);
console.log(invoices);
return <div className="mt-6 flow-root"></div>;
}
输入查询内容之后可以看到在控制台上面有日志输出
完成这些更改后,请继续进行测试。如果您搜索某个术语,您将更新URL,这将向服务器发送新请求,服务器将获取数据,并且仅返回与您的查询匹配的发票。
何时使用useSearchParams()钩子,何时使用searchParams道具?您可能已经注意到,您使用了两种不同的方法来提取搜索参数。使用哪种方法取决于您是在客户端还是在服务器上工作。
<Search>
是一个客户端组件,因此您使用useSearchParams()钩子从客户端访问参数。<Table>
是一个获取其自身数据的服务器组件,因此您可以将searchParamsprop
从页面传递到组件。
一般来说,如果您想从客户端读取参数,请使用use SearchParams()
钩子,因为这样可以避免必须返回服务器。
接下来我们需要实现表格,用于展示我们刚刚获取的数据内容,需要修改/app/ui/invoices/table.tsx
import Image from "next/image";
import { formatDateToLocal, formatCurrency } from "@/app/lib/utils";
import { fetchFilteredInvoices } from "@/app/lib/data";
export default async function InvoicesTable({
query,
currentPage,
}: {
query: string;
currentPage: number;
}) {
const invoices = await fetchFilteredInvoices(query, currentPage);
return (
<div className="mt-6 flow-root">
<div className="inline-block min-w-full align-middle">
<div className="rounded-lg bg-gray-50 p-2 md:pt-0">
<div className="md:hidden">
{invoices?.map((invoice) => (
<div
key={invoice.id}
className="mb-2 w-full rounded-md bg-white p-4"
>
<div className="flex items-center justify-between border-b pb-4">
<div>
<div className="mb-2 flex items-center">
<Image
src={invoice.image_url}
className="mr-2 rounded-full"
width={28}
height={28}
alt={`${invoice.name}'s profile picture`}
/>
<p>{invoice.name}</p>
</div>
<p className="text-sm text-gray-500">{invoice.email}</p>
</div>
</div>
<div className="flex w-full items-center justify-between pt-4">
<div>
<p className="text-xl font-medium">
{formatCurrency(invoice.amount)}
</p>
<p>{formatDateToLocal(invoice.date)}</p>
</div>
<div className="flex justify-end gap-2"></div>
</div>
</div>
))}
</div>
<table className="hidden min-w-full text-gray-900 md:table">
<thead className="rounded-lg text-left text-sm font-normal">
<tr>
<th scope="col" className="px-4 py-5 font-medium sm:pl-6">
Customer
</th>
<th scope="col" className="px-3 py-5 font-medium">
Email
</th>
<th scope="col" className="px-3 py-5 font-medium">
Amount
</th>
<th scope="col" className="px-3 py-5 font-medium">
Date
</th>
<th scope="col" className="px-3 py-5 font-medium">
Status
</th>
<th scope="col" className="relative py-3 pl-6 pr-3">
<span className="sr-only">Edit</span>
</th>
</tr>
</thead>
<tbody className="bg-white">
{invoices?.map((invoice) => (
<tr
key={invoice.id}
className="w-full border-b py-3 text-sm last-of-type:border-none [&:first-child>td:first-child]:rounded-tl-lg [&:first-child>td:last-child]:rounded-tr-lg [&:last-child>td:first-child]:rounded-bl-lg [&:last-child>td:last-child]:rounded-br-lg"
>
<td className="whitespace-nowrap py-3 pl-6 pr-3">
<div className="flex items-center gap-3">
<Image
src={invoice.image_url}
className="rounded-full"
width={28}
height={28}
alt={`${invoice.name}'s profile picture`}
/>
<p>{invoice.name}</p>
</div>
</td>
<td className="whitespace-nowrap px-3 py-3">
{invoice.email}
</td>
<td className="whitespace-nowrap px-3 py-3">
{formatCurrency(invoice.amount)}
</td>
<td className="whitespace-nowrap px-3 py-3">
{formatDateToLocal(invoice.date)}
</td>
<td className="whitespace-nowrap px-3 py-3"></td>
<td className="whitespace-nowrap py-3 pl-6 pr-3">
<div className="flex justify-end gap-3"></div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
);
}
现在,我们已经基本实现了查询表格内容了,接下来需要优化一下这个查询防抖
每次击键时,您都会更新URL,因此每次击键时都会查询数据库,这不是问题,因为我们的应用程序很小,但想象一下,如果您的应用程序有数千名用户,每个用户每次击键时都会向您的数据库发送一个新请求。
防抖是一种编程实践,它限制函数触发的频率。在我们的例子中,您只想在用户停止输入时查询数据库。
防抖工作原理:
触发事件:当发生需要去抖动的事件(例如搜索框中的击键)时,计时器启动。
等待:如果在计时器到期之前发生新事件,则重置计时器。
执行:如果计时器到达倒计时结束,则执行去抖动功能。
您可以通过几种方式实现去抖动,包括手动创建自己的去抖动函数。为了简单起见,我们将使用一个名为use-debounce。
安装use-debounce: pnpm i use-debounce
安装完成之后,修改/app/ui/search.tsx
const handleSearch = useDebouncedCallback((term: string) => {
const params = new URLSearchParams(searchParams);
if (term) {
params.set("query", term);
} else {
params.delete("query");
}
replace(`${pathname}?${params.toString()}`);
}, 300);
此函数将包装的内容handleSearch,并且仅在用户停止输入后的特定时间(300 毫秒)后运行代码,通过去抖动,您可以减少发送到数据库的请求数量,从而节省资源。
更详细内容查看
独立博客 https://www.dataeast.cn/
CSDN博客 https://blog.csdn.net/siberiaWarpDrive
B站视频空间 https://space.bilibili.com/25871614?spm_id_from=333.1007.0.0
关注 “曲速引擎 Warp Drive” 微信公众号