searchParamsが取得できない!? Next.js 16ではawaitが必要だった話
ページ内検索を作って完成したはずなのに、
検索結果には「記事がありません。」の文字。
URLには検索キーワードが入っているのに、
なぜかmicroCMSの記事を取得できませんでした。
今回は、そんなところから始まったデバッグの実話です。
「記事がありません。」から始まったデバッグ
Next.jsとmicroCMSを使って、 ブログの記事一覧ページに検索機能を付けていました。
検索フォームへキーワードを入力して、 「検索する」ボタンを押すとURLが切り替わります。
/blog?keyword=nextjs
URLにはきちんと
keyword=nextjsが付いています。
検索フォームも動いているように見えたので、 「これで完成かな」と思っていました。
ところが、画面に表示されたのは、 期待していた記事一覧ではありませんでした。
URLには検索キーワードがあるのに……
microCMSには「Next.js」を含む記事があります。
検索フォームからURLも変更されています。
それなのに、取得結果は0件でした。
最初に疑ったのは、microCMS側の設定です。
- 記事が公開状態になっているか
- 検索対象のフィールドに文字が入っているか
- APIキーやサービスドメインに間違いがないか
- 検索用のクエリ名が正しいか
しかし、検索条件を外すと記事一覧は正常に表示されます。
つまり、microCMSとの接続自体には問題がなさそうです。
そもそもsearchParamsとは?
searchParamsは、
URLのクエリパラメータをページ内で受け取るためのものです。
クエリパラメータとは、
URLの?より後ろに付いている情報です。
このURLをNext.jsのページで受け取ると、 解決後の値は次のようなイメージになります。
{
keyword: "nextjs",
page: "2"
}
検索キーワード以外にも、 ページ送り、並び順、カテゴリの絞り込みなどに利用できます。
?keyword=nextjs
?page=2
?category=news
?sort=publishedAt
App Routerのページでは、
URLのクエリパラメータを
searchParamsというpropsで受け取れます。
まず確認したのは「URLに値があるか」
検索が動かないとき、 最初に確認したのはブラウザーのURLでした。
検索フォームへ「nextjs」と入力して送信したあと、 URLが次のようになっているか確認します。
- 検索ボタンは反応している
- ページ遷移も行われている
- URLにkeywordが付いている
URLにキーワードが付いていなければ、 問題は検索フォーム側にある可能性が高くなります。
今回はURLに値が入っていたため、 検索フォームからページへ値を渡すところまでは成功していました。
nextjs
?keyword=nextjs
keywordが取れない
ここで、 ページ側のsearchParamsの読み取り方に 原因があるのではないかと考えました。
searchParamsが{}に見えた原因
原因を調べるため、
ページコンポーネントで
searchParamsを出力してみました。
type Props = {
searchParams: {
keyword?: string;
};
};
export default async function BlogPage({
searchParams,
}: Props) {
console.log(searchParams);
return <main>ブログ一覧</main>;
}
確認方法によっては、
出力された値が{}のように見えたり、
キーワードがundefinedになったりしました。
searchParams: {}
keyword: undefined
URLにはkeyword=nextjsがあるのに、
ページ側では値を取得できていません。
ここで重要なのは、 Next.js 16ではsearchParamsが通常のオブジェクトではなく、 Promiseとして渡されているという点でした。
まだ解決していない値
{ keyword: "nextjs" }
searchParamsが本当に空になったのではなく、 Promiseを解決する前に中身を確認・利用しようとしていたことが 問題でした。
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の書き方
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
searchParamsの型をPromiseにする
Promise<SearchParams>として 型を定義します。 -
2
ページ関数へasyncを付ける
関数内で
awaitを使うために必要です。 -
3
awaitしてから検索条件を取り出す
Promiseを解決したあとで、
keywordを確認します。
console.logで順番にデバッグしてみた
問題の場所を特定するため、 一度にすべてを確認するのではなく、 処理の途中へログを追加しました。
1.受け取ったPromiseを確認する
export default async function BlogPage({
searchParams,
}: Props) {
console.log(
"await前:",
searchParams
);
return <main>ブログ一覧</main>;
}
ここで確認しているのは、 検索条件の中身ではなくPromiseそのものです。
2.awaitしたあとの値を確認する
export default async function BlogPage({
searchParams,
}: Props) {
const resolvedSearchParams =
await searchParams;
console.log(
"await後:",
resolvedSearchParams
);
return <main>ブログ一覧</main>;
}
await後: {
keyword: "nextjs"
}
ここで初めて、 URLに入っていたキーワードを確認できました。
3.keywordだけを確認する
const keyword =
typeof resolvedSearchParams.keyword
=== "string"
? resolvedSearchParams.keyword
: "";
console.log("keyword:", keyword);
keyword: nextjs
4.microCMSへ渡すqueriesを確認する
const queries = {
q: keyword || undefined,
};
console.log("queries:", queries);
const data = await getArticles(queries);
queries: {
q: "nextjs"
}
?keyword=nextjs{ keyword: "nextjs" }"nextjs"{ q: "nextjs" }「検索できない」とまとめて考えるのではなく、 URL、searchParams、keyword、APIクエリの順に 値を追うと原因を見つけやすくなります。
awaitを使った修正方法
ここからは、実際の修正前と修正後を比較します。
修正前:searchParamsを直接読んでいた
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してから使う
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>
);
}
searchParams: Promise<SearchParams>
と定義します。
await searchParamsで
実際の検索条件を取り出します。
値が配列になる可能性も考え、
typeofで判定します。
trim()を使い、
空白だけの検索を防ぎます。
microCMSの検索へキーワードを渡す
microCMSのリストAPIでは、
全文検索用のクエリとしてqを渡せます。
Next.js側では、取得したキーワードを API呼び出し用の関数へ渡します。
microCMSクライアントと型を用意する
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へ渡す
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側の値だけでなく、検索対象フィールドに キーワードが含まれているかも確認します。
検索フォーム側の実装も確認する
ページ側を直したあとは、 検索フォームが正しいURLを作っているかも確認します。
今回は、入力やボタン操作を扱うため、 検索フォームをClient Componentとして作ります。
"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へ安全に組み立てられます。
ページ側から初期値を渡す
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>
);
}
入力値の管理、送信ボタン、 URLの変更を担当します。
URLの検索条件を受け取り、 microCMSから記事を取得します。
検索フォームの操作と、 APIからの記事取得を分けることで、 どこで問題が起きているか判断しやすくなりました。
よくあるつまずきと確認方法
Next.js 16のページでは、
Promise<...>として定義します。
type Props = {
searchParams: Promise<{
keyword?: string | string[];
}>;
};
先にPromiseを解決し、 その後で値を取り出します。
const resolvedSearchParams =
await searchParams;
URLが?keyword=nextjsなら、
読み取る名前もkeywordです。
resolvedSearchParams.keyword
同じクエリ名が複数ある場合、 値が配列になる可能性があります。
keyword?: string | string[];
Client ComponentでURLの検索条件を読む場合は、
useSearchParams()を使います。
"use client";
import {
useSearchParams,
} from "next/navigation";
const searchParams =
useSearchParams();
const keyword =
searchParams.get("keyword") ?? "";
Layoutはナビゲーション時に再レンダリングされないため、
最新の検索条件はPageの
searchParamsかClient Componentの
useSearchParams()で読みます。
今回のデバッグで分かったこと
最初は、microCMSの検索機能が正しく動いていないと思っていました。
しかし、順番に値を確認していくと、 問題はmicroCMSへリクエストを送る前に起きていました。
URLへkeywordを追加
awaitせずに使用
正しいqを受け取れない
検索結果が0件だったのは、 記事が存在しなかったからではありません。
searchParamsからキーワードを取得できず、
microCMSへ想定した検索条件を渡せていなかったことが原因でした。
「記事がありません。」と表示されても、 本当に記事がないとは限りません。 途中の値を確認し、どの段階で消えたのかを調べることが大切でした。
まとめ: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を学び始めたばかりだと、 書籍や以前の記事に書かれたコードと、 現在の仕様が違うことに気づきにくい場合があります。
けれど、エラーやログを一つずつ確認していくと、 「検索できない」という大きな問題も、 小さな処理へ分けて考えられるようになりました。
まずawaitを忘れていないか確認します。