本のコードが動かない!? Next.js 16で初心者が一番ハマった5つのポイント


Next.js・microCMS 初心者ノート

本のコードが動かない!? Next.js 16で初心者が一番ハマった5つのポイント

本のコードが動かない!?Next.js 16で初心者が一番ハマった5つのポイント

『Next.js+ヘッドレスCMSではじめる! かんたんモダンWebサイト制作入門』を 読みながら学習していたところ、書籍どおりに入力したはずなのに、 コードが動かない場面がいくつかありました。
原因は、書き間違いではなくNext.jsのバージョン差でした。

Next.js 16 microCMS バージョン差 初心者向け
01

本のコードが動かないのは、私のせい?

Next.jsとmicroCMSを使ったWebサイト制作を学ぶために、 『Next.js+ヘッドレスCMSではじめる! かんたんモダンWebサイト制作入門』を 読みながら、サンプルサイトを作っていました。

書籍の説明は分かりやすく、画面を一つずつ作っていけるため、 HTMLやCSSからNext.jsへ進みたい初心者にも取り組みやすい内容です。

ところが、途中から少しずつエラーが発生するようになりました。

?

書籍どおりに入力したはずなのに……

記事詳細ページが開かない。
検索キーワードを取得できない。
カテゴリ一覧を表示しようとすると型エラーになる。

はじめは、タグの閉じ忘れやスペルミスを疑いました。 コードを何度も見直しても、書籍のサンプルと同じように見えます。

調べて分かったのは、 書籍で使われているNext.jsと、私の環境に入っているNext.js 16では、 一部の書き方が変わっているということでした。

本のコードが動かないからといって、 必ずしも自分の入力が間違っているとは限りません。
フレームワークのバージョン差が原因になっていることもあります。

この記事では、実際に学習を進める中でつまずいた 5つのポイントを、初心者目線で整理します。

02

最初に確認したいNext.jsのバージョン

エラーの原因を調べる前に、まず確認したいのが 現在のプロジェクトで使用しているNext.jsのバージョンです。

ターミナルでプロジェクトのフォルダへ移動し、 次のコマンドを実行します。

Terminal
npm list next

また、プロジェクト内のpackage.jsonを開いて、 dependenciesに書かれているNext.jsのバージョンを確認する方法もあります。

package.json
{
  "dependencies": {
    "next": "16.0.0",
    "react": "19.2.0",
    "react-dom": "19.2.0"
  }
}
POINT

書籍、ブログ記事、動画教材などを参考にするときは、 サンプルコードだけでなく、使用されているNext.jsのバージョンも確認します。

Next.jsでは、バージョンアップによって推奨される書き方や APIの扱いが変わることがあります。

特に今回影響が大きかったのが、 paramssearchParamsの非同期化でした。

POINT 01

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してから値を取り出します。

Next.js 16に合わせた書き方
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で中身を取り出します。

POINT 02

searchParamsもPromiseになった

次につまずいたのが、ブログの検索ページでした。

例えば、次のようなURLで検索キーワードを渡すとします。

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してから検索条件を取得します。

Next.js 16に合わせた書き方
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>;
}

paramssearchParamsは名前が似ているため、 最初は同じものだと思ってしまいました。

名前
取得するもの
URLの例
params
URLのパス
/blog/hello
searchParams
URLのクエリ文字列
/blog?keyword=hello
POINT

動的フォルダの値を読むのがparams、 URLの?より後ろを読むのがsearchParamsです。

POINT 03

検索フォームと検索処理を分ける

ブログ検索を実装するとき、 入力欄とmicroCMSから記事を取得する処理を 一つのコンポーネントへまとめようとして混乱しました。

App Routerでは、何も指定しないコンポーネントは 基本的にServer Componentとして扱われます。

一方で、入力値を管理したり、ボタン操作でページを移動したりする部分には、 Client Componentが必要になることがあります。

Server Component 記事を取得して表示する

searchParamsを受け取り、 microCMSから条件に合う記事を取得します。

Client Component 入力やクリックを扱う

検索入力欄、ボタン操作、 URLの変更などを担当します。

検索ページ側でキーワードを受け取る

app/blog/page.tsx
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にする

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

このように、 データ取得はページ側、ユーザー操作はフォーム側と 役割を分けると理解しやすくなりました。

STEP 1 キーワードを入力

Client Component

STEP 2 URLを変更

?keyword=...

STEP 3 記事を取得

Server Component

POINT 04

検索条件が配列になる可能性がある

searchParamsの型を書くとき、 検索キーワードは文字列なのだから stringだけでよいと思っていました。

しかし、同じ名前の検索条件がURLに複数含まれる場合、 値が配列になる可能性があります。

URL /blog?keyword=nextjs&keyword=microcms

そのため、ページが受け取る検索条件は、 次のような型で定義しておくと安全です。

検索条件の型
type SearchParams = {
  keyword?: string | string[];
};

ただし、microCMSの検索条件へ渡したいのは一つの文字列です。 そこで、値が文字列かどうかを確認します。

文字列だけを取り出す
const keyword =
  typeof resolvedSearchParams.keyword === "string"
    ? resolvedSearchParams.keyword
    : "";
文字列の場合 そのまま使用する
配列・未指定の場合 空文字にする

TypeScriptの型エラーは、 初心者にとって少し怖く見えます。

けれど、今回のようなエラーは 「想定していない形のデータが届く可能性があります」と 事前に教えてくれているものでもあります。

初心者メモ

エラーを消すためだけにanyへ変更するのではなく、 どのような値が届く可能性があるのかを確認すると、 TypeScriptの意味が少しずつ分かってきます。

POINT 05

カテゴリ一覧と記事一覧を分けて考える

もう一つ迷ったのが、カテゴリ一覧の表示です。

microCMSに「ブログ」と「カテゴリ」のAPIを作っている場合、 それぞれは別のデータです。

BLOG ブログAPI

タイトル、本文、公開日、カテゴリ参照

CAT カテゴリAPI

カテゴリ名、カテゴリID

ブログ記事を取得した結果だけからカテゴリ一覧を作ろうとすると、 記事が一件も登録されていないカテゴリが表示されなかったり、 同じカテゴリが重複したりする可能性があります。

カテゴリナビゲーションを作る場合は、 カテゴリAPIからカテゴリ一覧を取得するほうが整理しやすくなります。

microCMSの型を定義する

libs/microcms.ts
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,
  });
};

カテゴリ一覧を取得して表示する

app/blog/page.tsx
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();
POINT

「記事を表示するためのデータ」と 「カテゴリメニューを表示するためのデータ」を分けて考えると、 APIから何を取得すればよいか判断しやすくなります。

08

コードが動かないときに確認したこと

Next.jsのエラーは、最初のうちは文章が長く、 どこを読めばよいのか分かりにくく感じます。

今回の経験から、次の順番で確認すると、 原因を探しやすいと感じました。

  1. 1
    エラーが出ているファイルを確認する

    ターミナルやブラウザーに表示されたファイル名と行番号を確認します。

  2. 2
    スペルと括弧を確認する

    タグ、丸括弧、波括弧、引用符の閉じ忘れを確認します。

  3. 3
    Next.jsのバージョンを確認する

    書籍や参考記事と、自分の環境のバージョンを比較します。

  4. 4
    公式ドキュメントで現在の型を確認する

    paramssearchParamsなど、 エラーに出ている名前で調べます。

  5. 5
    一度に全部直さない

    一か所ずつ変更し、エラー表示がどう変わったかを確認します。

コードが動かないと焦ってしまいますが、 エラーは「今の書き方では、ここを確認してください」という案内でもあります。
一つずつ読み解くことで、少しずつNext.jsの仕組みが見えてきました。

09

書籍が間違っているわけではない

今回、書籍のコードをそのまま使えない部分がありましたが、 これは書籍の内容そのものが間違っているという意味ではありません。

技術書は、執筆や検証を行った時点のバージョンをもとに作られています。 一方、Next.jsのようなフレームワークは、 書籍の発売後も更新されていきます。

書籍の制作時 当時のNext.jsで検証
現在 Next.js 16で学習
必要な対応 差分だけ調整する

書籍からは、ページ構成、コンポーネントの分け方、 microCMSとの接続方法など、今でも役立つ基本を学べます。

動かない部分だけを現在の公式仕様と照らし合わせて修正すれば、 書籍を学習の土台として十分活用できます。

大切なこと

書籍のコードを丸暗記するのではなく、 「このコードは何をするためのものか」を考えることで、 バージョンが変わったときにも対応しやすくなります。

10

まとめ:動かない経験も大切な学習になった

今回は、 『Next.js+ヘッドレスCMSではじめる! かんたんモダンWebサイト制作入門』を Next.js 16環境で進める中でつまずいたポイントをまとめました。

Next.js 16で確認した5つのポイント

  1. 1

    paramsはPromiseとして受け取り、 awaitしてから使う

  2. 2

    searchParamsもPromiseとして扱う

  3. 3

    検索フォームとデータ取得処理の役割を分ける

  4. 4

    検索条件はstring[]になる可能性も考える

  5. 5

    カテゴリ一覧と記事一覧は別のデータとして取得する

書籍どおりに入力したコードが動かないと、 「自分にはNext.jsは難しいのかもしれない」と不安になります。

けれど、原因がバージョン差だと分かると、 自分の理解不足だけが問題ではないことに気づけました。

そして、古い書き方と新しい書き方を見比べたことで、 paramssearchParamsが 何を担当しているのかも、以前より理解できるようになりました。

Next.js・microCMS初心者の学習記録 動かないところで立ち止まった時間も、
きっと理解を深めるための大切な時間です。


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

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

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