検索とページネーションの追加
前章では、ストリーミングを使用してダッシュボードの初期読み込みパフォーマンスを改善しました。今回は/invoices
ページに移動し、検索とページネーションを追加する方法を学びましょう。
開始コード
/dashboard/invoices/page.tsx
ファイル内に以下のコードを貼り付けます:
このページと作業するコンポーネントについて時間をかけて理解してください:
<Search/>
- ユーザーが特定の請求書を検索できる<Pagination/>
- ユーザーが請求書のページ間を移動できる<Table/>
- 請求書を表示する
検索機能はクライアントとサーバーの両方にまたがります。ユーザーがクライアントで請求書を検索すると、URLパラメータが更新され、サーバーでデータが取得され、テーブルが新しいデータでサーバー上で再レンダリングされます。
URL検索パラメータを使用する理由
前述のように、検索状態を管理するためにURL検索パラメータを使用します。クライアント側の状態でこれを行っていた場合、このパターンは新しいものかもしれません。
URLパラメータで検索を実装する利点はいくつかあります:
- ブックマーク可能で共有可能なURL:検索パラメータがURLにあるため、ユーザーは検索クエリやフィルタを含むアプリケーションの現在の状態をブックマークしたり共有したりできます。
- サーバーサイドレンダリング (SSR):URLパラメータはサーバー上で直接使用できるため、初期状態のレンダリングが容易になります。
- 分析と追跡:検索クエリとフィルタがURLに直接含まれているため、追加のクライアント側ロジックなしでユーザーの行動を追跡しやすくなります。
検索機能の追加
検索機能を実装するために使用するNext.jsクライアントフック:
useSearchParams
- 現在のURLのパラメータにアクセスできます。例えば、/dashboard/invoices?page=1&query=pending
の検索パラメータは{page: '1', query: 'pending'}
のようになります。usePathname
- 現在のURLのパス名を読み取ります。例えば、/dashboard/invoices
ルートの場合、usePathname
は'/dashboard/invoices'
を返します。useRouter
- クライアントコンポーネント内でプログラム的にルート間をナビゲートできます。使用できる複数のメソッドがあります。
実装手順の概要:
- ユーザーの入力をキャプチャする
- 検索パラメータでURLを更新する
- 入力フィールドとURLを同期させる
- 検索クエリを反映するようにテーブルを更新する
1. ユーザーの入力をキャプチャする
<Search>
コンポーネント(/app/ui/search.tsx
)を見ると、以下が確認できます:
"use client"
- これはクライアントコンポーネントで、イベントリスナーやフックを使用できます。<input>
- 検索入力フィールド
新しいhandleSearch
関数を作成し、<input>
要素にonChange
リスナーを追加します。onChange
は入力値が変更されるたびにhandleSearch
を呼び出します。
ブラウザの開発者ツールでコンソールを開き、検索フィールドに入力して正しく動作していることを確認してください。ブラウザコンソールに検索語句が表示されるはずです。
素晴らしい!ユーザーの検索入力をキャプチャできました。次に、検索語句でURLを更新する必要があります。
2. 検索パラメータでURLを更新する
next/navigation
からuseSearchParams
フックをインポートし、変数に割り当てます:
handleSearch
内で、searchParams
変数を使用して新しいURLSearchParams
インスタンスを作成します。
URLSearchParams
はURLクエリパラメータを操作するためのユーティリティメソッドを提供するWeb APIです。複雑な文字列リテラルを作成する代わりに、?page=1&query=a
のようなパラメータ文字列を取得できます。
次に、ユーザーの入力に基づいてparams文字列をset
します。入力が空の場合はdelete
します:
クエリ文字列を取得したので、Next.jsのuseRouter
とusePathname
フックを使用してURLを更新できます。
'next/navigation'
からuseRouter
とusePathname
をインポートし、handleSearch
内でuseRouter()
のreplace
メソッドを使用します:
ここで起こっていることの内訳:
${pathname}
は現在のパスで、この場合は"/dashboard/invoices"
- ユーザーが検索バーに入力すると、
params.toString()
はこの入力をURLフレンドリーな形式に変換します replace(${pathname}?${params.toString()})
はユーザーの検索データでURLを更新します。例えば、ユーザーが「Lee」を検索すると/dashboard/invoices?query=lee
になります- Next.jsのクライアントサイドナビゲーションにより、ページをリロードせずにURLが更新されます(ページ間のナビゲーションの章で学びました)
3. URLと入力を同期させる
入力フィールドがURLと同期し、共有時に値が入力されるようにするには、searchParams
から読み取ってdefaultValue
を入力に渡します:
defaultValue
vs.value
/ 制御 vs. 非制御入力の値を管理するために状態を使用している場合、
value
属性を使用して制御コンポーネントにします。これはReactが入力の状態を管理することを意味します。ただし、状態を使用していないため、
defaultValue
を使用できます。これはネイティブ入力が自身の状態を管理することを意味します。検索クエリを状態ではなくURLに保存しているため、これで問題ありません。
4. テーブルの更新
最後に、検索クエリを反映するようにテーブルコンポーネントを更新する必要があります。
請求書ページに戻ります。
ページコンポーネントはsearchParams
というpropを受け入れるため、現在のURLパラメータを<Table>
コンポーネントに渡せます。
<Table>
コンポーネントに移動すると、query
とcurrentPage
の2つのpropsがfetchFilteredInvoices()
関数に渡され、クエリに一致する請求書が返されることがわかります。
これらの変更を加えたら、テストしてみてください。検索語句を入力すると、URLが更新され、サーバーに新しいリクエストが送信され、サーバーでデータが取得され、クエリに一致する請求書のみが返されます。
useSearchParams()
フックとsearchParams
propの使い分け検索パラメータを抽出する2つの異なる方法に気付いたかもしれません。どちらを使用するかは、クライアント側で作業しているかサーバー側で作業しているかによって異なります。
<Search>
はクライアントコンポーネントなので、クライアントからパラメータにアクセスするためにuseSearchParams()
フックを使用しました<Table>
は自身のデータを取得するサーバーコンポーネントなので、ページからコンポーネントにsearchParams
propを渡せます一般的なルールとして、クライアントからパラメータを読み取りたい場合は、サーバーに戻る必要がないため、
useSearchParams()
フックを使用します。
ベストプラクティス: デバウンス
おめでとうございます!Next.jsで検索機能を実装できました!しかし、最適化できる点があります。
handleSearch
関数内に次のconsole.log
を追加してください:
次に検索バーに「Delba」と入力し、開発ツールのコンソールを確認してください。何が起こっていますか?
キーストロークごとにURLを更新し、そのたびにデータベースにクエリを送信しています!アプリケーションが小規模な場合は問題ありませんが、数千人のユーザーがいて、各キーストロークごとにデータベースにリクエストを送信する状況を想像してみてください。
デバウンス (debouncing) は、関数が実行される頻度を制限するプログラミングの手法です。今回の場合、ユーザーが入力を停止した時点でのみデータベースにクエリを送信したいのです。
デバウンスの仕組み:
- イベントトリガー: デバウンスすべきイベント(検索ボックスでのキーストロークなど)が発生すると、タイマーが開始します。
- 待機: タイマーが終了する前に新しいイベントが発生すると、タイマーがリセットされます。
- 実行: タイマーがカウントダウンを終了すると、デバウンスされた関数が実行されます。
デバウンスはいくつかの方法で実装できますが、ここではシンプルさを保つためにuse-debounce
ライブラリを使用します。
use-debounce
をインストール:
<Search>
コンポーネントでuseDebouncedCallback
関数をインポート:
この関数はhandleSearch
の内容をラップし、ユーザーが入力を停止してから指定時間(300ms)経過後にのみコードを実行します。
再度検索バーに入力し、開発ツールのコンソールを確認してください。次のように表示されるはずです:
デバウンスを導入することで、データベースに送信されるリクエスト数を減らし、リソースを節約できます。
ページネーションの追加
検索機能を導入した後、テーブルには一度に6つの請求書しか表示されないことに気づくでしょう。これはdata.ts
内のfetchFilteredInvoices()
関数が1ページあたり最大6つの請求書を返すためです。
ページネーションを追加することで、ユーザーはすべての請求書を表示するために異なるページ間を移動できます。検索と同様にURLパラメータを使用してページネーションを実装する方法を見てみましょう。
<Pagination/>
コンポーネントに移動すると、これがクライアントコンポーネントであることがわかります。クライアントでデータを取得したくありません(APIレイヤーを使用していないため、データベースの秘密が公開されてしまいます)。代わりに、サーバーでデータを取得し、それをプロップとしてコンポーネントに渡すことができます。
/dashboard/invoices/page.tsx
で、新しい関数fetchInvoicesPages
をインポートし、searchParams
からquery
を引数として渡します:
fetchInvoicesPages
は検索クエリに基づいて総ページ数を返します。例えば、検索クエリに一致する請求書が12件あり、各ページに6件表示する場合、総ページ数は2になります。
次に、totalPages
プロップを<Pagination/>
コンポーネントに渡します:
<Pagination/>
コンポーネントに移動し、usePathname
とuseSearchParams
フックをインポートします。これを使用して現在のページを取得し、新しいページを設定します。また、このコンポーネント内のコードのコメントを解除してください。<Pagination/>
のロジックをまだ実装していないため、アプリケーションは一時的に動作しなくなります。今すぐ実装しましょう!
次に、<Pagination>
コンポーネント内にcreatePageURL
という新しい関数を作成します。検索と同様に、URLSearchParams
を使用して新しいページ番号を設定し、pathName
を使用してURL文字列を作成します。
ここで起こっていることの内訳:
createPageURL
は現在の検索パラメータのインスタンスを作成します。- 次に、「page」パラメータを指定されたページ番号に更新します。
- 最後に、pathnameと更新された検索パラメータを使用して完全なURLを構築します。
<Pagination>
コンポーネントの残りの部分は、スタイリングとさまざまな状態(最初、最後、アクティブ、無効など)を扱います。このコースでは詳細には触れませんが、createPageURL
がどこで呼び出されているかを見るためにコードを自由に見てください。
最後に、ユーザーが新しい検索クエリを入力したときに、ページ番号を1にリセットしたい場合があります。これは<Search>
コンポーネントのhandleSearch
関数を更新することで実現できます:
まとめ
おめでとうございます!URL検索パラメータとNext.js APIを使用して検索とページネーションを実装しました。
この章で学んだことをまとめると:
- クライアント状態ではなくURL検索パラメータで検索とページネーションを処理しました。
- サーバー上でデータを取得しました。
- よりスムーズなクライアントサイド遷移のために
useRouter
ルーターフックを使用しました。
これらのパターンは、クライアントサイドReactで作業する際に慣れているものとは異なるかもしれませんが、URL検索パラメータを使用し、この状態をサーバーにリフトアップすることの利点をよりよく理解できたはずです。