Learn Next.js 官方教學筆記 - Chapter 11: Adding Search and Pagination
此系列為 Next.js 官方教學 Learn Next.js 的筆記。
文章連結:Chapter 11: Adding Search and Pagination
在上一篇文章裡,利用 streaming 的方式改善了 dashboard 最初載入的效能。接下來換成來處理 /invoices
的頁面,並學習如何加入搜尋功能以及頁碼。
在這一章節可以學到以下幾個項目:
- 學習如何使用 Next.js API:
searchParams
、usePathname
以及useRouter
。 - 利用 URL search params 實作搜尋以及頁碼。
Starting code
在 /dashboard/invoices/page.tsx 的檔案,貼上以下的程式碼:
// /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 { 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>
);
}
可以稍微熟悉一下這邊的程式碼,因為都是接下來會用到的 component:
<Search/>
可以讓使用者去搜尋特定的發票。<Pagination/>
可以讓使用者在發票頁面間移動。<Table/>
顯示發票的資訊。
搜尋的功能會包含 server 端以及 client 端的部分。當使用者在 client 搜尋發票時,URL params 會被更新,並且 data 會在 server 被 fetch,最後 table 會在 server 透過新的 data 來 re-render 。
Why use URL search params?
如同上面提到的,你可以使用 URL search params 來管理 search state。而若你以前比較常在 client 端處理這件事,這樣的 pattern 對你可能就會比較陌生。
那用 URL params 來實作搜尋功能是有一些好處的:
- 標記與分享 URLs:因為 search parameters 在 URL 的關係,使用者可以標記網頁目前的狀態,這包含了它們的 search queries、過濾條件等等,為了在未來可以參考或分享。
- Server-Side Rendering 以及最初的載入:URL parameters 可以在 server 上直接被使用來 render 最初的狀態,讓 server rendering 可以比較容易。
- 分析與追蹤:在 URL 上顯示 search queries 以及搜尋條件使用的方式,可以在不需要額外的 client 端邏輯的狀態下,讓追蹤使用者行為這件事變得比較簡單。
Adding the search functionality
以下是在 Next.js client 端你會用來實作搜尋功能的 hooks:
useSearchParams
:可以讓你拿到目前 URL 的參數。比如說:這個 URL 的 search params 為/dashboard/invoices?page=1&query=pending
會像這個{page: '1', query: 'pending’}
。usePathname
:可以讀取目前 URL 的路徑。比如說目前是這樣/dashboard/invoices
的 route ,usePathname
就會回傳'/dashboard/invoices'
。useRouter
:以 client 端程式的方式讓使用者在各個 route 間移動。這裡有多種方式可以實作。
以下快速的概覽實作的步驟:
- 取得使用者的輸入。
- 利用 search params 更新 URL 。
- 保持 URL 與輸入框的內容同步。
- 以搜尋參數更新 table 內容。
1. Capture the user's input
到 <Search>
Component (/app/ui/search.tsx
),你會注意到:
"use client"
- 代表這是一個 client coponent ,它可以讓你使用 event listeners以及 hooks。<input>
- 這是搜尋的輸入框。
建立一個新的 handleSearch
function,並增加 onChange
listener 到 element。當 input value 改變的時候就會利用onChange
來呼叫 handleSearch
。
// /app/ui/search.tsx
'use client';
import { MagnifyingGlassIcon } from '@heroicons/react/24/outline';
export default function Search({ placeholder }: { placeholder: string }) {
function handleSearch(term: string) { // ADD
console.log(term); // ADD
// ADD
return (
<div className="relative flex flex-1 flex-shrink-0">
<label htmlFor="search" className="sr-only">
Search
</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) => { // ADD
handleSearch(e.target.value); // ADD
}} // ADD
/>
<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>
);
}
可以在輸入文字的時候,利用 Dev tools 看看輸入的文字是不是有正常印出來。
2. Update the URL with the search params
從 'next/navigation'
import useSearchParams
hook,並 assign 給一個變數:
// /app/ui/search.tsx
'use client';
import { MagnifyingGlassIcon } from '@heroicons/react/24/outline';
import { useSearchParams } from 'next/navigation'; // ADD
export default function Search() {
const searchParams = useSearchParams(); // ADD
function handleSearch(term: string) {
console.log(term);
}
// ...
}
在 handleSearch 中,利用 searchParams 變數建立一個新的 URLSearchParams
實例,
// /app/ui/search.tsx
'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); // ADD
}
// ...
}
URLSearchParams
是一個 Web API,它可以提供多種方法來操作 URL query parameters。比起建立複雜的字符,用它可以取得像 ?page=1&query=a
的 params string。
根據使用者的 input 來設置 params string。判斷若 input 是空的,那就把它刪除的邏輯。
// /app/ui/search.tsx
'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) { // ADD
params.set('query', term); // ADD
} else { // ADD
params.delete('query'); // ADD
} // ADD
}
// ...
}
現在有了 query string. 你可以使用 Next.js 的 useRouter
以及 usePathname
hooks 來更新 URL。
從 'next/navigation'
Import useRouter
以及 usePathname
,並在 handleSearch
裡使用從 useRouter()
裡提出的 replace
方法。
'use client';
import { MagnifyingGlassIcon } from '@heroicons/react/24/outline';
import { useSearchParams, usePathname, useRouter } from 'next/navigation'; // Update
export default function Search() {
const searchParams = useSearchParams();
const pathname = usePathname(); // ADD
const { replace } = useRouter(); // ADD
function handleSearch(term: string) {
const params = new URLSearchParams(searchParams);
if (term) {
params.set('query', term);
} else {
params.delete('query');
}
replace(`${pathname}?${params.toString()}`); // ADD
}
}
以下解釋發生了什麼:
${pathname}
是表示目前的路徑,以範例來說,是"/dashboard/invoices"
.- 當使用者在輸入框輸入文字時,
params.toString()
會把輸入內容轉換成 URL-friendly 的格式, replace(${pathname}?${params.toString()})
會依照使用者輸入的資料來更新 URL。比方說,若使用者搜尋’Lee’
的話,就會是/dashboard/invoices?query=lee
。- 多虧 Next.js 的 client 端(會在 navigating between pages 的章節學到),URL的更新並不會讓頁面重新整理。
3. Keeping the URL and input in sync
為了確保輸入框可以與 URL 保持同步,並且要能在分享時也能自動填寫,你可以藉由讀取 searchParams
來把 defaultValue
傳遞給輸入框。
// /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()}
/>
defaultValue
vs.value
/ Controlled vs. Uncontrolled若你正使用 State 來管理輸入框的值,比較好的方式是使用
value
的屬性來控制 component。這代表 React 來管理輸入框的 State。然而,因為目前沒有使用到 State,你可以使用
defaultValue
。這代表輸入框會自己管理自己的 State。這樣的方式是可以的,因為目前已經把 search query 存到 URL 上,而不是使用 State。
4. Updating the table
再來是需要依據 search query 讓 table component 顯示相應的資料。
到 invoices 的頁面。Page component 接受叫做 searchParams 的 props,所以你可以傳遞當下的 URL params 到