2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【11-1】Next.js app routerのチュートリアルやってみる(URLパラメーターを利用した検索機能)

Last updated at Posted at 2024-02-07

はじめに

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のチュートリアルやってみる(部分的な事前レンダリング)

https://qiita.com/naoyuki2/items/8062f755b0679fe925b1

第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というメソッドを使用します

実装手順

  1. ユーザーが入力した検索キーワードを取得する
  2. 検索キーワードを使用して、URLを更新する
  3. URLと入力フィールドを同期させる
  4. 取得したデータを表示する

1. ユーザーが入力した検索キーワードを取得する

検索キーワードを入力する<search />コンポーネントを編集していきましょう。

/app/ui/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">
        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を更新しましょう。

/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) {
+     params.set('query', term);
+   } else {
+     params.delete('query');
+   }
  }
  // ...
}

useSearchParamsnext\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を更新しましょう。

/app/ui/search.tsx
'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から追加でusePathnameuseRouterimportしましょう。

usePathnameをつかって、現在のURLをpathnameに代入しましょう。

useRouterの中のreplaceというメソッドを使用するので分割代入しておきましょう。

replaceメソッドはURLを更新することができるメソッドです。

そして、pathnaemparams.toString()を結合して、新たなURLとして更新しています。

--- 例 ---
pathname = /dashboard/invoices
params = {'query','test'}
replace = /dashboard/invoices?query=test

3. URLと入力フィールドを同期させる

キーワードを検索したあとにページを再読み込みしたり、リンクからアクセスしたり、した場合にはinputタグの中身が空になってしまいます。

そのため下記のようにしてinputタグにデフォルトの値として現在の検索キーワードであるqueryの値を入れておきましょう。

/app/ui/search.tsx
<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 がinputstateを管理することを意味します。

ただし、stateを使用していないため、defaultValueを使用できます。これは、ネイティブinputが独自のstateを管理することを意味します。検索クエリをstateではなく URL に保存しているため、これは問題ありません。

4. 取得したデータを表示する

最後にデータを検索し、表示しましょう。

データを表示する<Table />コンポーネントに検索キーワードであるqueryと次回扱うpageを渡しています。

/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>
  );
}

Next.jsでは、page.tsxsearchParamsというPropsを受け取ることができます。

その中には以下のようにURLパラメーターが入っています。

image.png

普通、Propsはコンポーネントを呼び出す際に指定するものですが、これは例外のようですね。

そして、<Table />コンポーネント内でqueryを元にデータフェッチをしています。

/app/ui/invoices/table.tsx
// ...
export default async function InvoicesTable({
  query,
  currentPage,
}: {
  query: string;
  currentPage: number;
}) {
  const invoices = await fetchFilteredInvoices(query, currentPage);
  // ...
}

useSearchParamはクライアントコンポーネントでしか使用できないため、サーバーコンポーネントである、table.tsxにはPropsを通して、URLパラメーターを渡しています。

おまけ デバウンス

これでいったん検索機能は完成です。

ですが、今のままではキーストローク(キーボードを押すこと)ごとにデータフェッチが行われてしまいます。

Emilと検索しようとしても、計4回データフェッチをしてしまいます。

image.png

そこでデバウンスという技術を使用します。

デバウンスとは、関数が起動できる速度を制限するプログラミング手法です。

この例では、ユーザーが入力をやめたときにのみデータベースにクエリを実行する必要があります。

image.png

これを実装するためにuse-debounceというライブラリを利用します。

ターミナル
npm i use-debounce

<Search />コンポーネントにuseDebouncedCallbackという関数をimportします。

/app/ui/search.tsx
// ...
+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個に分けました。

大変だけど、力にはなってるはず。

次の記事

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?