searchParamsが取得できない!?Next.js 16ではawaitが必要だった話


Next.js・microCMS 初心者ノート

searchParamsが取得できない!? Next.js 16ではawaitが必要だった話

searchParamsが取得できない?Next.js 16でawaitが必要だった話

ページ内検索を作って完成したはずなのに、 検索結果には「記事がありません。」の文字。
URLには検索キーワードが入っているのに、 なぜかmicroCMSの記事を取得できませんでした。
今回は、そんなところから始まったデバッグの実話です。

Next.js 16 App Router searchParams microCMS 検索機能 初心者向け
01

「記事がありません。」から始まったデバッグ

Next.jsとmicroCMSを使って、 ブログの記事一覧ページに検索機能を付けていました。

検索フォームへキーワードを入力して、 「検索する」ボタンを押すとURLが切り替わります。

URL /blog?keyword=nextjs

URLにはきちんと keyword=nextjsが付いています。

検索フォームも動いているように見えたので、 「これで完成かな」と思っていました。

ところが、画面に表示されたのは、 期待していた記事一覧ではありませんでした。

0
検索結果 記事がありません。
?

URLには検索キーワードがあるのに……

microCMSには「Next.js」を含む記事があります。
検索フォームからURLも変更されています。
それなのに、取得結果は0件でした。

最初に疑ったのは、microCMS側の設定です。

  • 記事が公開状態になっているか
  • 検索対象のフィールドに文字が入っているか
  • APIキーやサービスドメインに間違いがないか
  • 検索用のクエリ名が正しいか

しかし、検索条件を外すと記事一覧は正常に表示されます。

つまり、microCMSとの接続自体には問題がなさそうです。

✿

「記事が取得できない」のではなく、
「検索キーワードがmicroCMSへ正しく渡っていないのでは?」 と考えることにしました。

02

そもそもsearchParamsとは?

searchParamsは、 URLのクエリパラメータをページ内で受け取るためのものです。

クエリパラメータとは、 URLの?より後ろに付いている情報です。

https://example.com/blog?keyword=nextjs&page=2
ページのパス 検索キーワード ページ番号

このURLをNext.jsのページで受け取ると、 解決後の値は次のようなイメージになります。

searchParamsの中身
{
  keyword: "nextjs",
  page: "2"
}

検索キーワード以外にも、 ページ送り、並び順、カテゴリの絞り込みなどに利用できます。

SEARCH キーワード検索 ?keyword=nextjs
PAGE ページ送り ?page=2
CATEGORY カテゴリ絞り込み ?category=news
SORT 並び順 ?sort=publishedAt
POINT

App Routerのページでは、 URLのクエリパラメータを searchParamsというpropsで受け取れます。

03

まず確認したのは「URLに値があるか」

検索が動かないとき、 最初に確認したのはブラウザーのURLでした。

検索フォームへ「nextjs」と入力して送信したあと、 URLが次のようになっているか確認します。

https://example.com/blog?keyword=nextjs
確認できたこと
  • 検索ボタンは反応している
  • ページ遷移も行われている
  • URLにkeywordが付いている

URLにキーワードが付いていなければ、 問題は検索フォーム側にある可能性が高くなります。

今回はURLに値が入っていたため、 検索フォームからページへ値を渡すところまでは成功していました。

1 入力

nextjs

›
2 URL

?keyword=nextjs

›
3 ページ側

keywordが取れない

ここで、 ページ側のsearchParamsの読み取り方に 原因があるのではないかと考えました。

04

searchParamsが{}に見えた原因

原因を調べるため、 ページコンポーネントで searchParamsを出力してみました。

確認用 app/blog/page.tsx
type Props = {
  searchParams: {
    keyword?: string;
  };
};

export default async function BlogPage({
  searchParams,
}: Props) {
  console.log(searchParams);

  return <main>ブログ一覧</main>;
}

確認方法によっては、 出力された値が{}のように見えたり、 キーワードがundefinedになったりしました。

Terminal
searchParams: {}
keyword: undefined

URLにはkeyword=nextjsがあるのに、 ページ側では値を取得できていません。

ここで重要なのは、 Next.js 16ではsearchParamsが通常のオブジェクトではなく、 Promiseとして渡されているという点でした。

受け取ったもの Promise

まだ解決していない値

≠
使いたかったもの 検索条件のオブジェクト

{ keyword: "nextjs" }

正確には

searchParamsが本当に空になったのではなく、 Promiseを解決する前に中身を確認・利用しようとしていたことが 問題でした。

05

Next.js 16で変わったこと

Next.js 14以前のサンプルコードでは、 searchParamsを同期的なオブジェクトとして 直接扱っている場合があります。

以前の書き方

以前 同期的に取得する例
type Props = {
  searchParams: {
    keyword?: string;
  };
};

export default function BlogPage({
  searchParams,
}: Props) {
  const keyword =
    searchParams.keyword ?? "";

  return <p>検索語:{keyword}</p>;
}

このコードでは、 searchParams.keywordへ直接アクセスしています。

Next.js 16の書き方

現在 Promiseをawaitする例
type SearchParams = {
  keyword?: string | string[];
};

type Props = {
  searchParams: Promise<SearchParams>;
};

export default async function BlogPage({
  searchParams,
}: Props) {
  const resolvedSearchParams =
    await searchParams;

  const keyword =
    typeof resolvedSearchParams.keyword
      === "string"
      ? resolvedSearchParams.keyword
      : "";

  return <p>検索語:{keyword}</p>;
}
修正前 searchParams.keyword
→
修正後 await searchParams

変更点を整理すると、次の3つです。

  1. 1
    searchParamsの型をPromiseにする

    Promise<SearchParams>として 型を定義します。

  2. 2
    ページ関数へasyncを付ける

    関数内でawaitを使うために必要です。

  3. 3
    awaitしてから検索条件を取り出す

    Promiseを解決したあとで、 keywordを確認します。

✿

Next.js 16では、同期的な searchParams.keywordではなく、 await searchParamsが基本です。

06

console.logで順番にデバッグしてみた

問題の場所を特定するため、 一度にすべてを確認するのではなく、 処理の途中へログを追加しました。

1.受け取ったPromiseを確認する

awaitする前の確認
export default async function BlogPage({
  searchParams,
}: Props) {
  console.log(
    "await前:",
    searchParams
  );

  return <main>ブログ一覧</main>;
}

ここで確認しているのは、 検索条件の中身ではなくPromiseそのものです。

2.awaitしたあとの値を確認する

解決後のsearchParams
export default async function BlogPage({
  searchParams,
}: Props) {
  const resolvedSearchParams =
    await searchParams;

  console.log(
    "await後:",
    resolvedSearchParams
  );

  return <main>ブログ一覧</main>;
}
Terminal
await後: {
  keyword: "nextjs"
}

ここで初めて、 URLに入っていたキーワードを確認できました。

3.keywordだけを確認する

keywordの確認
const keyword =
  typeof resolvedSearchParams.keyword
    === "string"
    ? resolvedSearchParams.keyword
    : "";

console.log("keyword:", keyword);
Terminal
keyword: nextjs

4.microCMSへ渡すqueriesを確認する

APIへ渡す直前の値
const queries = {
  q: keyword || undefined,
};

console.log("queries:", queries);

const data = await getArticles(queries);
Terminal
queries: {
  q: "nextjs"
}
確認場所
期待する値
分かること
URL
?keyword=nextjs
フォーム送信の成否
await後
{ keyword: "nextjs" }
ページで取得できたか
keyword
"nextjs"
型判定が正しいか
queries
{ q: "nextjs" }
APIへ渡せているか
デバッグのコツ

「検索できない」とまとめて考えるのではなく、 URL、searchParams、keyword、APIクエリの順に 値を追うと原因を見つけやすくなります。

07

awaitを使った修正方法

ここからは、実際の修正前と修正後を比較します。

修正前:searchParamsを直接読んでいた

修正前 app/blog/page.tsx
import { getArticles } from "@/libs/microcms";

type Props = {
  searchParams: {
    keyword?: string;
  };
};

export default async function BlogPage({
  searchParams,
}: Props) {
  const keyword =
    searchParams.keyword ?? "";

  const data = await getArticles({
    q: keyword,
  });

  return (
    <main>
      <h1>ブログ記事一覧</h1>

      {data.contents.length > 0 ? (
        data.contents.map((article) => (
          <article key={article.id}>
            <h2>{article.title}</h2>
          </article>
        ))
      ) : (
        <p>記事がありません。</p>
      )}
    </main>
  );
}

このコードは、 searchParamsを通常のオブジェクトとして 直接読んでいます。

修正後:Promiseをawaitしてから使う

修正後 app/blog/page.tsx
import { getArticles } from "@/libs/microcms";

type SearchParams = {
  keyword?: string | string[];
};

type Props = {
  searchParams: Promise<SearchParams>;
};

export default async function BlogPage({
  searchParams,
}: Props) {
  const resolvedSearchParams =
    await searchParams;

  const keyword =
    typeof resolvedSearchParams.keyword
      === "string"
      ? resolvedSearchParams.keyword.trim()
      : "";

  const data = await getArticles({
    q: keyword || undefined,
  });

  return (
    <main>
      <h1>ブログ記事一覧</h1>

      {keyword && (
        <p>
          「{keyword}」の検索結果:
          {data.totalCount}件
        </p>
      )}

      {data.contents.length > 0 ? (
        data.contents.map((article) => (
          <article key={article.id}>
            <h2>{article.title}</h2>
          </article>
        ))
      ) : (
        <p>
          条件に一致する記事がありません。
        </p>
      )}
    </main>
  );
}
1
Promise型へ変更

searchParams: Promise<SearchParams> と定義します。

2
awaitで解決

await searchParamsで 実際の検索条件を取り出します。

3
文字列か確認

値が配列になる可能性も考え、 typeofで判定します。

4
空文字を除外

trim()を使い、 空白だけの検索を防ぎます。

08

microCMSの検索へキーワードを渡す

microCMSのリストAPIでは、 全文検索用のクエリとしてqを渡せます。

Next.js側では、取得したキーワードを API呼び出し用の関数へ渡します。

microCMSクライアントと型を用意する

libs/microcms.ts
import {
  createClient,
  MicroCMSQueries,
} from "microcms-js-sdk";

export type Article = {
  id: string;
  title: string;
  description?: string;
};

const client = createClient({
  serviceDomain:
    process.env.MICROCMS_SERVICE_DOMAIN!,
  apiKey:
    process.env.MICROCMS_API_KEY!,
});

export const getArticles = async (
  queries?: MicroCMSQueries
) => {
  return client.getList<Article>({
    endpoint: "blogs",
    queries,
  });
};

検索キーワードをqへ渡す

app/blog/page.tsx
const resolvedSearchParams =
  await searchParams;

const keyword =
  typeof resolvedSearchParams.keyword
    === "string"
    ? resolvedSearchParams.keyword.trim()
    : "";

const data = await getArticles({
  q: keyword || undefined,
});

キーワードが空の場合は、 undefinedを渡しています。

これにより、キーワードが指定されていない通常のブログ一覧では、 検索条件なしで記事を取得できます。

キーワードあり { q: "nextjs" } 一致する記事を検索
キーワードなし { q: undefined } 通常の記事一覧を取得
注意

microCMSの検索結果が0件だった場合、 Next.js側の値だけでなく、検索対象フィールドに キーワードが含まれているかも確認します。

09

検索フォーム側の実装も確認する

ページ側を直したあとは、 検索フォームが正しいURLを作っているかも確認します。

今回は、入力やボタン操作を扱うため、 検索フォームをClient Componentとして作ります。

components/SearchForm.tsx
"use client";

import {
  FormEvent,
  useState,
} from "react";

import { useRouter } from "next/navigation";

type Props = {
  defaultKeyword?: string;
};

export default function SearchForm({
  defaultKeyword = "",
}: Props) {
  const router = useRouter();

  const [keyword, setKeyword] =
    useState(defaultKeyword);

  const handleSubmit = (
    event: FormEvent<HTMLFormElement>
  ) => {
    event.preventDefault();

    const trimmedKeyword =
      keyword.trim();

    if (!trimmedKeyword) {
      router.push("/blog");
      return;
    }

    const query =
      new URLSearchParams({
        keyword: trimmedKeyword,
      });

    router.push(
      `/blog?${query.toString()}`
    );
  };

  return (
    <form onSubmit={handleSubmit}>
      <label htmlFor="keyword">
        記事を検索
      </label>

      <input
        id="keyword"
        type="search"
        value={keyword}
        onChange={(event) => {
          setKeyword(event.target.value);
        }}
        placeholder="キーワードを入力"
      />

      <button type="submit">
        検索する
      </button>
    </form>
  );
}

URLSearchParamsを使うと、 検索キーワードをURLへ安全に組み立てられます。

ページ側から初期値を渡す

app/blog/page.tsx
import SearchForm
  from "@/components/SearchForm";

export default async function BlogPage({
  searchParams,
}: Props) {
  const resolvedSearchParams =
    await searchParams;

  const keyword =
    typeof resolvedSearchParams.keyword
      === "string"
      ? resolvedSearchParams.keyword
      : "";

  return (
    <main>
      <SearchForm
        defaultKeyword={keyword}
      />
    </main>
  );
}
Client Component SearchForm

入力値の管理、送信ボタン、 URLの変更を担当します。

›
Server Component BlogPage

URLの検索条件を受け取り、 microCMSから記事を取得します。

検索フォームの操作と、 APIからの記事取得を分けることで、 どこで問題が起きているか判断しやすくなりました。

10

よくあるつまずきと確認方法

CHECK 01 searchParamsをPromise型にしていない

Next.js 16のページでは、 Promise<...>として定義します。

type Props = {
  searchParams: Promise<{
    keyword?: string | string[];
  }>;
};
CHECK 02 awaitより前にkeywordを読んでいる

先にPromiseを解決し、 その後で値を取り出します。

const resolvedSearchParams =
  await searchParams;
CHECK 03 URLとプロパティ名が一致していない

URLが?keyword=nextjsなら、 読み取る名前もkeywordです。

resolvedSearchParams.keyword
CHECK 04 string[]の可能性を考えていない

同じクエリ名が複数ある場合、 値が配列になる可能性があります。

keyword?: string | string[];
CHECK 05 Client Componentと混同している

Client ComponentでURLの検索条件を読む場合は、 useSearchParams()を使います。

"use client";

import {
  useSearchParams,
} from "next/navigation";

const searchParams =
  useSearchParams();

const keyword =
  searchParams.get("keyword") ?? "";
CHECK 06 layout.tsxで読もうとしている

Layoutはナビゲーション時に再レンダリングされないため、 最新の検索条件はPageの searchParamsかClient Componentの useSearchParams()で読みます。

✿

URLに値があるか、await後に値があるか、 APIへ渡す直前に値があるか。
この3か所を確認するだけでも、 検索機能のデバッグがかなり進めやすくなります。

11

今回のデバッグで分かったこと

最初は、microCMSの検索機能が正しく動いていないと思っていました。

しかし、順番に値を確認していくと、 問題はmicroCMSへリクエストを送る前に起きていました。

問題なし 検索フォーム

URLへkeywordを追加

›
原因 searchParams

awaitせずに使用

›
影響 microCMS

正しいqを受け取れない

検索結果が0件だったのは、 記事が存在しなかったからではありません。

searchParamsからキーワードを取得できず、 microCMSへ想定した検索条件を渡せていなかったことが原因でした。

!
表示結果だけで原因を決めない

「記事がありません。」と表示されても、 本当に記事がないとは限りません。 途中の値を確認し、どの段階で消えたのかを調べることが大切でした。

12

まとめ:searchParamsはawaitしてから使う

今回は、検索機能を作ったのに 「記事がありません。」と表示されてしまったところから、 デバッグを始めました。

URLにはキーワードが入っていたため、 最初はmicroCMS側の問題だと思っていました。

しかし、原因はNext.js 16で searchParamsがPromiseとして渡されることを 理解できていなかった点にありました。

今回覚えておきたいこと

  • searchParamsはURLのクエリパラメータを受け取る
  • Next.js 16ではsearchParamsがPromiseとして渡される
  • Server Componentでは await searchParamsが必要
  • await後のオブジェクトからkeywordを取得する
  • 値はstring | string[] | undefinedを考慮する
  • URL、await後、API送信前の順でログを確認する
  • Client Componentでは useSearchParams()を使う
最後に、この基本形を覚えておきます
type SearchParams = {
  keyword?: string | string[];
};

type Props = {
  searchParams: Promise<SearchParams>;
};

export default async function BlogPage({
  searchParams,
}: Props) {
  const resolvedSearchParams =
    await searchParams;

  const keyword =
    typeof resolvedSearchParams.keyword
      === "string"
      ? resolvedSearchParams.keyword.trim()
      : "";

  return (
    <p>
      検索キーワード:{keyword}
    </p>
  );
}

Next.jsを学び始めたばかりだと、 書籍や以前の記事に書かれたコードと、 現在の仕様が違うことに気づきにくい場合があります。

けれど、エラーやログを一つずつ確認していくと、 「検索できない」という大きな問題も、 小さな処理へ分けて考えられるようになりました。

Next.js・microCMS初心者の学習記録 URLに値があるのに取得できないときは、
まずawaitを忘れていないか確認します。


AILPENGINE。自動進化型AI-LP。公開後も、成果に合わせて進化する! 行動計測、分析、構成改善、成果最大化。自前計測、CTA位置最適化、レポート可視化。LP改善を相談する(相談無料)。

日本ドラフトのAIチャットボットで問い合わせ対応を効率化・自動化!24時間365日対応、簡単設定、マルチプラットフォーム対応で人件費・工数を削減。今すぐ無料で相談する

※当社は、当サイトに掲載する情報について、正確性・完全性・有用性その他一切の事項を保証するものではありません。
また、当サイトの情報をご利用いただいたことにより生じた損害、ならびに当サイトからリンクまたはバナー等を通じて移動した外部サイトにおいて提供される情報・サービス等により生じた損害について、当社は一切の責任を負いかねます。