本のコードが動かない!? Next.js 16で初心者が一番ハマった5つのポイント
『Next.js+ヘッドレスCMSではじめる! かんたんモダンWebサイト制作入門』を
読みながら学習していたところ、書籍どおりに入力したはずなのに、
コードが動かない場面がいくつかありました。
原因は、書き間違いではなくNext.jsのバージョン差でした。
本のコードが動かないのは、私のせい?
Next.jsとmicroCMSを使ったWebサイト制作を学ぶために、 『Next.js+ヘッドレスCMSではじめる! かんたんモダンWebサイト制作入門』を 読みながら、サンプルサイトを作っていました。
書籍の説明は分かりやすく、画面を一つずつ作っていけるため、 HTMLやCSSからNext.jsへ進みたい初心者にも取り組みやすい内容です。
ところが、途中から少しずつエラーが発生するようになりました。
書籍どおりに入力したはずなのに……
記事詳細ページが開かない。
検索キーワードを取得できない。
カテゴリ一覧を表示しようとすると型エラーになる。
はじめは、タグの閉じ忘れやスペルミスを疑いました。 コードを何度も見直しても、書籍のサンプルと同じように見えます。
調べて分かったのは、 書籍で使われているNext.jsと、私の環境に入っているNext.js 16では、 一部の書き方が変わっているということでした。
この記事では、実際に学習を進める中でつまずいた 5つのポイントを、初心者目線で整理します。
最初に確認したいNext.jsのバージョン
エラーの原因を調べる前に、まず確認したいのが 現在のプロジェクトで使用しているNext.jsのバージョンです。
ターミナルでプロジェクトのフォルダへ移動し、 次のコマンドを実行します。
npm list next
また、プロジェクト内のpackage.jsonを開いて、
dependenciesに書かれているNext.jsのバージョンを確認する方法もあります。
{
"dependencies": {
"next": "16.0.0",
"react": "19.2.0",
"react-dom": "19.2.0"
}
}
書籍、ブログ記事、動画教材などを参考にするときは、 サンプルコードだけでなく、使用されているNext.jsのバージョンも確認します。
Next.jsでは、バージョンアップによって推奨される書き方や APIの扱いが変わることがあります。
特に今回影響が大きかったのが、
paramsとsearchParamsの非同期化でした。
paramsをそのまま使えない
最初につまずいたのは、ブログの記事詳細ページです。
Next.jsのApp Routerでは、フォルダ名を角括弧で囲むことで、 URLの一部が変わる動的なページを作れます。
src
└── app
└── blog
└── [slug]
└── page.tsx
例えば、URLが/blog/html-and-tsxの場合、
html-and-tsxの部分がslugとして渡されます。
以前のサンプルでは、次のようにparams.slugを
直接取得している場合があります。
type Props = {
params: {
slug: string;
};
};
export default async function Page({
params,
}: Props) {
const slug = params.slug;
return <p>{slug}</p>;
}
Next.js 16では、ページへ渡されるparamsを
Promiseとして型定義し、awaitしてから値を取り出します。
type Props = {
params: Promise<{
slug: string;
}>;
};
export default async function Page({
params,
}: Props) {
const { slug } = await params;
return <p>{slug}</p>;
}
params.slug
const { slug } = await params
初心者の私には、 URLから受け取る値がなぜPromiseになるのかが、最初はよく分かりませんでした。
まずは難しく考えすぎず、 「Next.js 16のServer Componentでは、paramsをawaitしてから使う」 と覚えると整理しやすくなりました。
paramsの型にPromise<...>を付け、
ページ関数をasyncにして、
await paramsで中身を取り出します。
searchParamsもPromiseになった
次につまずいたのが、ブログの検索ページでした。
例えば、次のようなURLで検索キーワードを渡すとします。
/blog?keyword=nextjs
このURLにあるkeyword=nextjsの部分を、
Next.jsのページではsearchParamsから取得できます。
以前のコードでは、次のように
searchParams.keywordを直接使用している場合があります。
type Props = {
searchParams: {
keyword?: string;
};
};
export default async function Page({
searchParams,
}: Props) {
const keyword = searchParams.keyword ?? "";
return <p>検索語:{keyword}</p>;
}
Next.js 16では、searchParamsもPromiseとして受け取り、
awaitしてから検索条件を取得します。
type SearchParams = {
keyword?: string | string[];
};
type Props = {
searchParams: Promise<SearchParams>;
};
export default async function Page({
searchParams,
}: Props) {
const resolvedSearchParams = await searchParams;
const keyword =
typeof resolvedSearchParams.keyword === "string"
? resolvedSearchParams.keyword
: "";
return <p>検索語:{keyword}</p>;
}
paramsとsearchParamsは名前が似ているため、
最初は同じものだと思ってしまいました。
params/blog/hellosearchParams/blog?keyword=hello
動的フォルダの値を読むのがparams、
URLの?より後ろを読むのがsearchParamsです。
検索フォームと検索処理を分ける
ブログ検索を実装するとき、 入力欄とmicroCMSから記事を取得する処理を 一つのコンポーネントへまとめようとして混乱しました。
App Routerでは、何も指定しないコンポーネントは 基本的にServer Componentとして扱われます。
一方で、入力値を管理したり、ボタン操作でページを移動したりする部分には、 Client Componentが必要になることがあります。
searchParamsを受け取り、
microCMSから条件に合う記事を取得します。
検索入力欄、ボタン操作、 URLの変更などを担当します。
検索ページ側でキーワードを受け取る
import SearchForm from "@/components/SearchForm";
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
: "";
const data = await getArticles({
q: keyword || undefined,
});
return (
<main>
<h1>ブログ</h1>
<SearchForm defaultKeyword={keyword} />
{data.contents.map((article) => (
<article key={article.id}>
<h2>{article.title}</h2>
</article>
))}
</main>
);
}
入力フォームは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?keyword=${encodeURIComponent(
trimmedKeyword
)}`
);
return;
}
router.push("/blog");
};
return (
<form onSubmit={handleSubmit}>
<input
type="search"
value={keyword}
onChange={(event) =>
setKeyword(event.target.value)
}
placeholder="記事を検索"
/>
<button type="submit">
検索する
</button>
</form>
);
}
このように、 データ取得はページ側、ユーザー操作はフォーム側と 役割を分けると理解しやすくなりました。
Client Component
?keyword=...
Server Component
検索条件が配列になる可能性がある
searchParamsの型を書くとき、
検索キーワードは文字列なのだから
stringだけでよいと思っていました。
しかし、同じ名前の検索条件がURLに複数含まれる場合、 値が配列になる可能性があります。
/blog?keyword=nextjs&keyword=microcms
そのため、ページが受け取る検索条件は、 次のような型で定義しておくと安全です。
type SearchParams = {
keyword?: string | string[];
};
ただし、microCMSの検索条件へ渡したいのは一つの文字列です。 そこで、値が文字列かどうかを確認します。
const keyword =
typeof resolvedSearchParams.keyword === "string"
? resolvedSearchParams.keyword
: "";
TypeScriptの型エラーは、 初心者にとって少し怖く見えます。
けれど、今回のようなエラーは 「想定していない形のデータが届く可能性があります」と 事前に教えてくれているものでもあります。
エラーを消すためだけにanyへ変更するのではなく、
どのような値が届く可能性があるのかを確認すると、
TypeScriptの意味が少しずつ分かってきます。
カテゴリ一覧と記事一覧を分けて考える
もう一つ迷ったのが、カテゴリ一覧の表示です。
microCMSに「ブログ」と「カテゴリ」のAPIを作っている場合、 それぞれは別のデータです。
タイトル、本文、公開日、カテゴリ参照
カテゴリ名、カテゴリID
ブログ記事を取得した結果だけからカテゴリ一覧を作ろうとすると、 記事が一件も登録されていないカテゴリが表示されなかったり、 同じカテゴリが重複したりする可能性があります。
カテゴリナビゲーションを作る場合は、 カテゴリAPIからカテゴリ一覧を取得するほうが整理しやすくなります。
microCMSの型を定義する
import {
createClient,
MicroCMSQueries,
} from "microcms-js-sdk";
export type Category = {
id: string;
name: string;
};
export type Article = {
id: string;
title: string;
category?: Category;
};
const client = createClient({
serviceDomain:
process.env.MICROCMS_SERVICE_DOMAIN!,
apiKey:
process.env.MICROCMS_API_KEY!,
});
export const getCategories = async (
queries?: MicroCMSQueries
) => {
return client.getList<Category>({
endpoint: "categories",
queries,
});
};
export const getArticles = async (
queries?: MicroCMSQueries
) => {
return client.getList<Article>({
endpoint: "blogs",
queries,
});
};
カテゴリ一覧を取得して表示する
import Link from "next/link";
import {
getArticles,
getCategories,
} from "@/libs/microcms";
export default async function BlogPage() {
const [articleData, categoryData] =
await Promise.all([
getArticles(),
getCategories(),
]);
return (
<main>
<nav aria-label="カテゴリ一覧">
<ul>
{categoryData.contents.map(
(category) => (
<li key={category.id}>
<Link
href={`/category/${category.id}`}
>
{category.name}
</Link>
</li>
)
)}
</ul>
</nav>
<div>
{articleData.contents.map(
(article) => (
<article key={article.id}>
<h2>{article.title}</h2>
</article>
)
)}
</div>
</main>
);
}
Promise.all()を使うと、
記事一覧とカテゴリ一覧の取得を並行して進められます。
ただし、初心者のうちは無理に一行へまとめず、 まずは次のように別々に書いて動きを確認しても問題ありません。
const articleData = await getArticles();
const categoryData = await getCategories();
「記事を表示するためのデータ」と 「カテゴリメニューを表示するためのデータ」を分けて考えると、 APIから何を取得すればよいか判断しやすくなります。
コードが動かないときに確認したこと
Next.jsのエラーは、最初のうちは文章が長く、 どこを読めばよいのか分かりにくく感じます。
今回の経験から、次の順番で確認すると、 原因を探しやすいと感じました。
-
1
エラーが出ているファイルを確認する
ターミナルやブラウザーに表示されたファイル名と行番号を確認します。
-
2
スペルと括弧を確認する
タグ、丸括弧、波括弧、引用符の閉じ忘れを確認します。
-
3
Next.jsのバージョンを確認する
書籍や参考記事と、自分の環境のバージョンを比較します。
-
4
公式ドキュメントで現在の型を確認する
paramsやsearchParamsなど、 エラーに出ている名前で調べます。 -
5
一度に全部直さない
一か所ずつ変更し、エラー表示がどう変わったかを確認します。
書籍が間違っているわけではない
今回、書籍のコードをそのまま使えない部分がありましたが、 これは書籍の内容そのものが間違っているという意味ではありません。
技術書は、執筆や検証を行った時点のバージョンをもとに作られています。 一方、Next.jsのようなフレームワークは、 書籍の発売後も更新されていきます。
書籍からは、ページ構成、コンポーネントの分け方、 microCMSとの接続方法など、今でも役立つ基本を学べます。
動かない部分だけを現在の公式仕様と照らし合わせて修正すれば、 書籍を学習の土台として十分活用できます。
書籍のコードを丸暗記するのではなく、 「このコードは何をするためのものか」を考えることで、 バージョンが変わったときにも対応しやすくなります。
まとめ:動かない経験も大切な学習になった
今回は、 『Next.js+ヘッドレスCMSではじめる! かんたんモダンWebサイト制作入門』を Next.js 16環境で進める中でつまずいたポイントをまとめました。
Next.js 16で確認した5つのポイント
-
1
paramsはPromiseとして受け取り、awaitしてから使う -
2
searchParamsもPromiseとして扱う -
3
検索フォームとデータ取得処理の役割を分ける
-
4
検索条件は
string[]になる可能性も考える -
5
カテゴリ一覧と記事一覧は別のデータとして取得する
書籍どおりに入力したコードが動かないと、 「自分にはNext.jsは難しいのかもしれない」と不安になります。
けれど、原因がバージョン差だと分かると、 自分の理解不足だけが問題ではないことに気づけました。
そして、古い書き方と新しい書き方を見比べたことで、
paramsやsearchParamsが
何を担当しているのかも、以前より理解できるようになりました。
きっと理解を深めるための大切な時間です。