Next.js 16で書籍のコードが動かない? paramsがPromiseになった変更点を解説
Next.jsの書籍を見ながらコードを書いたのに、
params.slugの部分でエラーになってしまいました。
原因を調べてみると、Next.jsの新しいバージョンでは
paramsがPromiseとして扱われるようになっていました。
書籍どおりなのにコードが動かない
Next.jsとmicroCMSを使ったWebサイト制作を勉強するために、 書籍のサンプルコードを見ながら記事詳細ページを作っていました。
フォルダを作り、書籍に書かれているコードを入力して、 ブラウザーで記事ページを開いてみます。
ところが、画面には期待していた記事が表示されず、
ターミナルやエディターには
paramsに関するエラーが表示されました。
書籍と同じように入力したはずなのに……
スペルを間違えたのかな?
フォルダ名が違うのかな?
microCMSの設定に問題があるのかな?
コードを何度見直しても、書籍の内容と大きな違いはありません。
そこでNext.jsの現在の仕様を調べたところ、 原因は入力ミスではなく、 書籍と現在のNext.jsとのバージョン差にありました。
今回問題になったのは、
App Routerの動的ルートで使用する
paramsの扱い方です。
そもそもparamsとは?
paramsは、URLの一部から値を受け取るために使います。
例えば、microCMSで作ったブログに、 次のような記事詳細ページがあるとします。
https://example.com/blog/html-and-tsx
このURLでは、最後の
html-and-tsxが記事を識別するための値です。
Next.jsのApp Routerでは、角括弧を使ったフォルダを作ることで、 URLによって内容が変わる動的ルートを作れます。
src
└── app
└── blog
└── [slug]
└── page.tsx
[slug]の部分には、
実際のURLに含まれる値が入ります。
/blog/html-and-tsx{ slug: "html-and-tsx" }/blog/nextjs-basic{ slug: "nextjs-basic" }/blog/microcms-start{ slug: "microcms-start" }
paramsは、URLの動的な部分を
Next.jsのページ内で受け取るための値です。
Next.jsで何が変わったの?
Next.js 14以前では、
ページに渡されたparamsを
通常のオブジェクトとして直接使用できました。
type Props = {
params: {
slug: string;
};
};
export default function Page({ params }: Props) {
const slug = params.slug;
return <p>{slug}</p>;
}
この書き方では、paramsを受け取ったあと、
すぐにparams.slugへアクセスしています。
一方、現在のNext.jsでは、
paramsはPromiseとしてページへ渡されます。
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
つまり、大きな変更点は次の3つです。
-
paramsの型をPromise<...>にする -
ページ関数に
asyncを付ける -
await paramsで値を取り出す
「Next.js 16で突然変わった」というより、 Next.js 15で導入された非同期化が、 Next.js 16でも正式な現在仕様として使われています。
Promiseとは?初心者向けにやさしく整理
paramsがPromiseになったと聞いても、
JavaScriptやTypeScriptを学び始めたばかりだと、
何が変わったのか分かりにくいかもしれません。
Promiseは簡単にいうと、 あとで受け取れる値を表す仕組みです。
まだ中身を直接使わない
値が使える状態になるまで待つ
slugを利用できる
例えば、次のコードでは
getMessage()からPromiseが返ってきます。
async function getMessage() {
return "こんにちは";
}
async function showMessage() {
const message = await getMessage();
console.log(message);
}
awaitを付けることで、
Promiseの中から実際の値である
「こんにちは」を取り出しています。
paramsについても考え方は同じです。
const resolvedParams = await params;
const slug = resolvedParams.slug;
分割代入を使うと、これを一行で書けます。
const { slug } = await params;
なぜawait paramsが必要なの?
Promiseの状態で渡されたparamsは、
そのままでは通常のオブジェクトと同じように扱えません。
そのため、次のように直接プロパティへアクセスするのではなく、 先にPromiseを解決する必要があります。
const slug = params.slug;
const { slug } = await params;
awaitを使う関数には、
原則としてasyncを付けます。
export default async function Page({
params,
}: Props) {
const { slug } = await params;
return <p>{slug}</p>;
}
async
関数内でawaitを使えるようにする
await
Promiseが解決されるまで待って値を取り出す
{ slug }
解決後のオブジェクトからslugだけを取り出す
書籍のコードを修正前・修正後で比較
実際にどこを変更すればよいのか、 記事詳細ページのコード全体で比較します。
修正前:paramsを同期オブジェクトとして扱う
type Props = {
params: {
slug: string;
};
};
export default function BlogDetailPage({
params,
}: Props) {
return (
<main>
<h1>記事詳細ページ</h1>
<p>記事ID:{params.slug}</p>
</main>
);
}
修正後:paramsをPromiseとして扱う
type Props = {
params: Promise<{
slug: string;
}>;
};
export default async function BlogDetailPage({
params,
}: Props) {
const { slug } = await params;
return (
<main>
<h1>記事詳細ページ</h1>
<p>記事ID:{slug}</p>
</main>
);
}
{ slug: string }を
Promise<{ slug: string }>へ変更します。
関数内でawaitを使用するために必要です。
Promiseを解決してから、
slugを画面やAPI処理で使用します。
修正するのは、型だけではありません。
asyncとawaitも
セットで追加する必要があります。
microCMSの記事詳細ページで使う例
microCMSの記事詳細ページでは、
URLから受け取ったslugや
コンテンツIDを使って記事データを取得します。
ここでは、URLの値をmicroCMSのコンテンツIDとして使用する例を見てみます。
src
├── app
│ └── blog
│ └── [id]
│ └── page.tsx
└── libs
└── microcms.ts
microCMSから記事を取得する関数
import { createClient } from "microcms-js-sdk";
export type Blog = {
id: string;
title: string;
content: string;
};
const client = createClient({
serviceDomain:
process.env.MICROCMS_SERVICE_DOMAIN!,
apiKey:
process.env.MICROCMS_API_KEY!,
});
export const getBlogDetail = async (
contentId: string
) => {
return client.getListDetail<Blog>({
endpoint: "blogs",
contentId,
});
};
paramsからIDを取得してAPIへ渡す
import { getBlogDetail } from "@/libs/microcms";
type Props = {
params: Promise<{
id: string;
}>;
};
export default async function BlogDetailPage({
params,
}: Props) {
const { id } = await params;
const article = await getBlogDetail(id);
return (
<main>
<h1>{article.title}</h1>
<div
dangerouslySetInnerHTML={{
__html: article.content,
}}
/>
</main>
);
}
/blog/abc123
{ id: "abc123" }
このコードで重要なのは、
microCMSへIDを渡す前に
await paramsを実行していることです。
paramsそのものをmicroCMSへ渡すのではなく、
Promiseを解決して取り出した
idやslugを渡します。
generateMetadataでも同じ対応が必要
記事ごとにページタイトルや説明文を変える場合、
Next.jsのgenerateMetadataを使用できます。
この関数でparamsを使う場合も、
Promiseとして受け取り、awaitする必要があります。
import type { Metadata } from "next";
import { getBlogDetail } from "@/libs/microcms";
type Props = {
params: Promise<{
id: string;
}>;
};
export async function generateMetadata({
params,
}: Props): Promise<Metadata> {
const { id } = await params;
const article = await getBlogDetail(id);
return {
title: article.title,
description:
`${article.title}の記事詳細ページです。`,
};
}
export default async function BlogDetailPage({
params,
}: Props) {
const { id } = await params;
const article = await getBlogDetail(id);
return (
<main>
<h1>{article.title}</h1>
</main>
);
}
ページ本体だけを修正しても、
generateMetadata側で古い書き方が残っていると、
別の場所でエラーになる可能性があります。
page.tsxのページ関数layout.tsxのレイアウト関数generateMetadata- Route Handlerで動的ルートを使っている箇所
プロジェクト内でparams.を検索すると、
古い書き方が残っている場所を見つけやすくなります。
Client Componentではどうするの?
ここまで紹介したのは、
App RouterのServer Componentで
paramsを受け取る書き方です。
Client Componentで現在の動的ルートを読みたい場合は、
useParamsを使用する方法があります。
"use client";
import { useParams } from "next/navigation";
export default function CurrentArticleId() {
const params = useParams<{
id: string;
}>();
return (
<p>
現在の記事ID:{params.id}
</p>
);
}
const { id } = await params
const params = useParams()
microCMSから記事を取得してページを表示するだけであれば、
基本的にはServer Component側で
await paramsを使う形が分かりやすいです。
修正しても動かないときの確認ポイント
await paramsへ修正してもエラーが残る場合は、
次の項目を確認します。
awaitを使う関数には、
asyncが必要です。
export default async function Page() {
// awaitを使用できます
}
型だけが以前の書き方のままになっていないか確認します。
params: Promise<{
slug: string;
}>;
params.slugへ直接アクセスしている箇所が
残っていないか確認します。
const { slug } = await params;
フォルダが[id]なら、
paramsのプロパティもidです。
// フォルダ:app/blog/[id]/page.tsx
type Props = {
params: Promise<{
id: string;
}>;
};
型や環境設定を変更したあとも表示が変わらない場合は、 開発サーバーを停止して起動し直します。
npm run dev
書籍が間違っているわけではない
書籍のサンプルコードが現在の環境で動かないと、 「本の内容が間違っているのでは?」と思ってしまうかもしれません。
しかし、技術書は執筆時点や検証時点のバージョンをもとに作られています。
Next.jsのように更新が続くフレームワークでは、 書籍の発売後に仕様が変わることがあります。
ページ構成やmicroCMSとの連携方法など、 書籍から学べる基本的な考え方はそのまま活用できます。
動かない部分だけを現在の仕様に合わせて修正することで、 書籍を学習教材として引き続き利用できます。
まとめ:paramsはawaitしてから使う
Next.jsの書籍を読みながら記事詳細ページを作っていると、
paramsの部分でコードが動かないことがあります。
その原因の一つが、
Next.jsのバージョンアップによる
paramsの非同期化です。
今回覚えておきたいポイント
-
paramsは動的なURLの値を受け取るもの -
現在のApp Routerでは
paramsがPromiseとして渡される -
型を
Promise<...>に変更する -
ページ関数へ
asyncを付ける -
const { slug } = await paramsで値を取得する -
generateMetadataなどでも同様に対応する
type Props = {
params: Promise<{
slug: string;
}>;
};
export default async function Page({
params,
}: Props) {
const { slug } = await params;
return <p>{slug}</p>;
}
書籍どおりに書いたコードが動かないと、 自分の理解や入力に問題があるように感じてしまいます。
けれど、今回のようにバージョン差が原因になっていることもあります。
エラーメッセージと現在の公式仕様を確認しながら修正することで、 コードを動かすだけでなく、 Next.jsの仕組みも少しずつ理解できるようになりました。
asyncとawaitをセットで覚えておきます。