本文へ移動
verified onnext@16.3.5react@19
最終検証日 2026-09-26
Next.js道場

Next.js の検索(useSearchParams)。最小構成サンプル

検索でようやく Client Component が1つ必要になります。

動いているものを触る/demo/search

ファイル構成

並んでいるのは、上の画面をいま動かしている本物のファイルです。名前を押すと中身が出ます。

ファイル構成
  • app/
app/layout.tsxBuild

全ページの外側。本物のプロジェクトでは html と body をここで書きます

import type { Metadata } from "next";
 
export const metadata: Metadata = {
  title: "検索の最小構成",
};
 
export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return <main>{children}</main>;
}
.tsx
画面を返すファイル。HTML のような書き方ができます
.ts
画面を返さないファイル。処理とデータだけを置きます。ここに画面を書くと原因を名指ししないエラーで落ちます
Build
ビルド時に一度だけ動きます
Server
アクセスのたびにサーバーで動きます
Browser
利用者の手元で動きます

ここで初めて1つ要る

入出力もログインも一覧と詳細も、Client Component は1つも要りませんでした。検索で初めて1つ必要になります。

必要になる理由は、打つたびに反応させたいからです。送信ボタンを押す形でよければ、ここも素のフォームで足ります。

どちらも正しい。反応の速さと、送るものの量の取り引き
ボタンを押す形
// Client Component は要らない
<form>
  <input name="q" />
  <button type="submit">探す</button>
</form>

ブラウザへ送るコードは増えない。押すまで結果は変わらない。

打つたびに反応する形
// 入力欄だけが Client Component になる
'use client'
 
export function SearchBox({ value }) {
  const router = useRouter()
  return <input defaultValue={value} onChange={...} />
}

入力欄のぶんだけコードが増える。打つたびに結果が変わる。

増やすのは葉だけ

検索の画面で、どちら側に何が置かれているか
サーバーに残るもの
絞り込みの処理
元のデータ8件
絞る条件
ブラウザに届くもの
入力欄の見た目と動き
絞ったあとの結果

越えられる: 検索語(アドレスとして)

越えられる: 絞った結果

越えられない: 元のデータ

越えられない: 絞り込みの処理

実機で確かめました。ビルドしたものを検査すると、元のデータは配信される JavaScript に含まれていません。含まれていたのは、入力欄に書いた placeholder の文字だけでした。境界がそのまま成果物に出ています。

絞り込みはサーバーでやる

入力欄がやっているのは、アドレスを書き換えることだけです。

router.replace(q ? `?q=${encodeURIComponent(q)}` : '?')

絞り込みそのものは、サーバー側のページがやります。

const { q } = await searchParams
const query = String(q ?? '').slice(0, 40)
const found = await searchTerms(query)

実機で確かめました。何も入れなければ 8 件、use で 5 件、server で 2 件、zzz で 0 件になります。元の8件はブラウザへ行っていません。

1文字打ってから、結果が変わるまで
  1. Browser
    アドレスを書き換える

    入力欄がやるのはここまで。絞り込みはしない。

  2. Server
    検索語を受け取って絞る

    元のデータはここにある。絞ってから返す。

  3. Browser
    結果だけが差し替わる

    入力欄は入れ替わらないので、打った文字は消えない。

読むのはサーバーの仕事

useSearchParams を使えば、ブラウザ側でも検索語を読めます。このサンプルでは使っていません。

読むのはサーバーがもう済ませていて、入力欄には切ったあとの値を props で渡しています。ブラウザ側でもう一度アドレスを読むと、切る前の値を使うことになります。

Client Component が要る理由は、読むためではなく書き換えるためです。打つたびにアドレスを変えるのは、ブラウザ側にしかできません。

それでも useSearchParams を使うなら

「Suspense で囲め」と書かれているのを見たことがあるかもしれません。囲めと言われるのは、そのページが静的なままのときです。

ビルド時にページを作ろうとしたとき、検索語はまだ存在しません。そこで手が止まるので、Next.js は待つ場所を要求します。それが Suspense です。

このページのように searchParams を自分で読んでいるページは、その時点でもう動的なので要求されません。実機で確かめました。

囲まずにビルドが落ちる形と、その文言はこの問題にあります。 「とりあえず囲む」ではなく、「そのページが静的か動的か」で決めてください。

ここから足していくとき

  • 入力を間引く。いまは1文字ごとにサーバーへ行きます。実務では少し待ってから送ります
  • 見つからなかったときの案内。0件のときに何を出すかは、検索の質そのものです
  • データベースへ。searchTerms の中を置き換えるだけで、ページ側は変わりません

変える場所はいつも app/data.ts の中か、切り出した葉の部分です。ページを Client Component にする必要は、ここでも出てきません。

入れていないもの

このまま本番に持っていかないでください。省いてあるのは次の点です。

  • 入力の間引き。1文字ごとにサーバーへ行くので、実務では待ち時間を入れます
  • 検索語の細かい検証。長さを40文字で切っているだけです
  • データベース。8件の配列を絞っているだけです
  • 件数が多いときのページ送りと、見つからなかったときの案内
  • 検索語の記録。何が探されたかは残していません

出典

このページの内容は上記の一次情報を基準に書いています。個人ブログや二次情報を根拠にしていません。

理解できたか試す

では、このコードはどうなりますか。

検索語を読むだけの小さな部品です。ページに置いたらビルドが失敗しました。原因はどれですか。

この問題を解くこのテーマの問題 3 問