はじめに
Next.js app routerのチュートリアルの第11-1章のアウトプットをします。
前の記事
【01】Next.js app routerのチュートリアルやってみる
https://qiita.com/naoyuki2/items/af58da3d20cbc790e767
【02】Next.js app routerのチュートリアルやってみる
https://qiita.com/naoyuki2/items/edf450b3ee135e83d1e8
【03】Next.js app routerのチュートリアルやってみる
https://qiita.com/naoyuki2/items/612221eac233aa9cbb74
【04】Next.js app routerのチュートリアルやってみる
https://qiita.com/naoyuki2/items/62f9beccbfe36eaf7f90
【05】Next.js app routerのチュートリアルやってみる
https://qiita.com/naoyuki2/items/8b71b1d1df7c9435a9c9
【06】Next.js app routerのチュートリアルやってみる
https://qiita.com/naoyuki2/items/58130c3cfbaf8a573de2
【07】Next.js app routerのチュートリアルやってみる
https://qiita.com/naoyuki2/items/2c2da0f8071e60454679
【08】Next.js app routerのチュートリアルやってみる
https://qiita.com/naoyuki2/items/45f45fcb9cc14506f79f
【09】Next.js app routerのチュートリアルやってみる(loading.tsxとSuspenseでストリーミング)
https://qiita.com/naoyuki2/items/717694288ec6017a3af2
【10】Next.js app routerのチュートリアルやってみる(部分的な事前レンダリング)
第11-1章 URLパラメーターを利用した検索機能
この章では下記を学習しました。
- URL検索パラメータを使用して、検索機能とページネーション機能を実装
-
searchParams
,usePathname
,useRouter
の使い方 - デバウンスの実装
検索機能が結構重たかったので二つに分けることにしました。
今回は検索機能のみ取り扱います。
今回作業する場所
.
└── invoices/
└── page.tsx/
├── <Search />
├── <Pagenation />
└── <Table />
/dashboard/invoices/page.tsx
とその中にimport
している3つのコンポーネントをいじります。
-
<Search />
--- いわゆる検索バーがあるコンポーネント -
<pagenation />
--- ページネーションを実装するコンポーネント -
<Table />
--- 検索結果を表示するコンポーネント
検索機能
今回の検索機能はURLパラメーターを使用して実装します。
URLパラメーターを使う利点
検索機能にURLパラメーターを使うことによって以下のような利点があります。
- ブックマークが可能になる
- URLを共有できる
- URLパラメーターはサーバー側でも利用可能なため、初期ロード時間を短縮できる
(再読み込みしたときにサーバー側に検索キーワードがあるから?) - ユーザーがどの検索条件をよく使うかなどの分析がしやすくなる
(Google Analyticsなどを使用するとユーザーが訪れたURLとパラメータを追跡できるため)
使用するフック
--- usePathname
---
現在アクセスしているパスを取得することができます
例:localhost:3000/dashboard/invoices
--> /dashboard/invoices
を返す
--- useSearchParams
---
現在のURLのパラメーターにアクセスできます
例:/dashboard/invoices?page=1&query=pending
--> {page: '1', query: 'pending'}
を返す
--- useRouter
---
クライアントコンポーネント内でルーティングに関する機能を使用できます
今回はその中でもreplace
というメソッドを使用します
実装手順
- ユーザーが入力した検索キーワードを取得する
- 検索キーワードを使用して、URLを更新する
- URLと入力フィールドを同期させる
- 取得したデータを表示する
1. ユーザーが入力した検索キーワードを取得する
検索キーワードを入力する<search />
コンポーネントを編集していきましょう。
+'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">
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) => {
+ 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>
);
}
ユーザーの入力内容を取得するには、input
タグにonChange
イベントを付けましょう。
そして、それによって発火するhandleSearch
関数を書きましょう。
onChange
イベントはクライアントコンポーネントでしか使えないので、use client
を忘れないようにしましょう。
これでinput
タグに入力した内容がコンソール
に出力されるようになりました。
2. 検索キーワードを使用して、URLを更新する
続いて、URLを更新しましょう。
'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');
+ }
}
// ...
}
useSearchParams
をnext\navigation
からimport
しましょう。
そして、searchParams
という定数に現在のURLパラメーターを代入しましょう。
現在のURLは/dashboard/invoices
なので、{}
が代入されます。
URLSearchParams
は、URLパラメーターの整形を楽にするためのWebAPI
です。(これだけではURLの更新をすることはできません。詳しく)
これに現在のURLパラメーターであるsearchParams
を渡してインスタンスを作成しましょう。
ユーザーの検索キーワードはterm
という名前で受け取っています。
term
に検索キーワードが入っていたら、params.set('query, trem')
によって、名前がquery
で、値がtrem
である、URLパラメーターを作成することができます。
例:test
が入力された --> /dashborad/invoices?query=test
となる
そして、もしterm
が空だったらparams.delele('query')
によって、名前がquery
であるURLパラメーターを削除します。
これによって、params
にURLパラメーターが代入されたので、それによって下のコードでURLを更新しましょう。
'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()}`);
}
}
next/navigation
から追加でusePathname
とuseRouter
をimport
しましょう。
usePathname
をつかって、現在のURLをpathname
に代入しましょう。
useRouter
の中のreplace
というメソッドを使用するので分割代入しておきましょう。
replace
メソッドはURLを更新することができるメソッドです。
そして、pathnaem
とparams.toString()
を結合して、新たなURLとして更新しています。
--- 例 ---
pathname
= /dashboard/invoices
params
= {'query','test'}
replace = /dashboard/invoices?query=test
3. URLと入力フィールドを同期させる
キーワードを検索したあとにページを再読み込みしたり、リンクからアクセスしたり、した場合にはinput
タグの中身が空になってしまいます。
そのため下記のようにしてinput
タグにデフォルトの値として現在の検索キーワードであるquery
の値を入れておきましょう。
<input
className="..."
placeholder={placeholder}
onChange={(e) => {
handleSearch(e.target.value);
}}
+ defaultValue={searchParams.get('query')?.toString()}
/>
defaultValue
ではなく、value
としてしまうと、下のようなエラーが出てしまうので注意しましょう。
Warning: A component is changing an uncontrolled input to be controlled. This is likely caused by the value changing from undefined to a defined value, which should not happen. Decide between using a controlled or uncontrolled input element for the lifetime of the component.
defaultValueとvalue 制御されたものと制御されていないもの
state
を使用して入力の値を管理している場合は、value属性を使用してそれを制御コンポーネントにします。これは、React がinput
のstate
を管理することを意味します。ただし、
state
を使用していないため、defaultValue
を使用できます。これは、ネイティブinput
が独自のstate
を管理することを意味します。検索クエリをstate
ではなく URL に保存しているため、これは問題ありません。
4. 取得したデータを表示する
最後にデータを検索し、表示しましょう。
データを表示する<Table />
コンポーネントに検索キーワードであるquery
と次回扱うpage
を渡しています。
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>
);
}
Next.js
では、page.tsx
でsearchParams
というProps
を受け取ることができます。
その中には以下のようにURLパラメーターが入っています。
普通、Props
はコンポーネントを呼び出す際に指定するものですが、これは例外のようですね。
そして、<Table />
コンポーネント内でquery
を元にデータフェッチをしています。
// ...
export default async function InvoicesTable({
query,
currentPage,
}: {
query: string;
currentPage: number;
}) {
const invoices = await fetchFilteredInvoices(query, currentPage);
// ...
}
useSearchParam
はクライアントコンポーネントでしか使用できないため、サーバーコンポーネントである、table.tsx
にはProps
を通して、URLパラメーターを渡しています。
おまけ デバウンス
これでいったん検索機能は完成です。
ですが、今のままではキーストローク(キーボードを押すこと)ごとにデータフェッチが行われてしまいます。
Emil
と検索しようとしても、計4回データフェッチをしてしまいます。
そこでデバウンス
という技術を使用します。
デバウンス
とは、関数が起動できる速度を制限するプログラミング手法です。
この例では、ユーザーが入力をやめたときにのみデータベースにクエリを実行する必要があります。
これを実装するためにuse-debounce
というライブラリを利用します。
npm i use-debounce
<Search />
コンポーネントにuseDebouncedCallback
という関数をimport
します。
// ...
+import { useDebouncedCallback } from 'use-debounce';
// Inside the Search Component...
+const handleSearch = useDebouncedCallback((term) => {
console.log(`Searching... ${term}`);
const params = new URLSearchParams(searchParams);
if (term) {
params.set('query', term);
} else {
params.delete('query');
}
replace(`${pathname}?${params.toString()}`);
+}, 300);
デバウンス
を実装したい関数(handleSearch
)をuseDebounceCallback
でラップします。
そして、ユーザーが入力をやめてから300ミリ秒が経過した後にこのコードを実行します。
おわりに
11章の検索機能は結構重たかったので、2個に分けました。
大変だけど、力にはなってるはず。
次の記事