🦔
# Next.js 15でparamsが非同期になった理由と対応方法
Next.js 15でparamsが非同期になった理由と対応方法
はじめに
Next.js 15にアップデートしたら、突然こんなエラーが出て困った経験はありませんか?
Type error: Type 'Props' does not satisfy the constraint 'PageProps'.
Types of property 'params' are incompatible.
Type '{ pattern: string; }' is missing the following properties from type 'Promise<any>': then, catch, finally
この記事では、Next.js 15でparams
が非同期になった理由と、具体的な対応方法を解説します。
何が変わったのか
Next.js 14まで(従来)
// pages/results/[pattern]/page.tsx
interface Props {
params: {
pattern: string;
};
}
export default function ResultPage({ params }: Props) {
const pattern = parseInt(params.pattern); // 同期的にアクセス可能
// ...
}
Next.js 15以降(新仕様)
// app/results/[pattern]/page.tsx
interface Props {
params: Promise<{
pattern: string;
}>;
}
export default async function ResultPage({ params }: Props) {
const { pattern: patternString } = await params; // awaitが必要
const pattern = parseInt(patternString);
// ...
}
主な変更点:
-
params
がPromise
型になった - ページコンポーネントを
async
にする必要がある -
await params
でパラメータを取得する
なぜこんなめんどくさい変更をしたのか
1. ストリーミングレンダリングの最適化
従来のNext.jsでは、すべてのパラメータが解決されてからページがレンダリングされていました。
// 従来:すべて同期的
function Page({ params, searchParams }) {
// params と searchParams が両方揃うまで待機
return <div>{params.id}</div>;
}
新しい仕組みでは、必要な部分だけを先にレンダリングできます。
// 新仕様:必要な部分から順次レンダリング
async function Page({ params, searchParams }) {
// ヘッダーなど、paramsに依存しない部分は先にレンダリング
const header = <Header />;
// パラメータが必要な部分だけawait
const { id } = await params;
const content = <Content id={id} />;
return <>{header}{content}</>;
}
2. パフォーマンスの向上
Time to First Byte (TTFB) の改善:
// 従来
Request → Wait for all params → Render → Response
|--------|------------------|-------|--------|
100ms 200ms 50ms 50ms = 400ms
// 新仕様
Request → Start Render → Await params → Complete
|--------|------------|-------------|---------|
100ms 50ms 200ms 50ms = 400ms
↑
ここで部分的なレスポンスを開始
3. 動的ルーティングの複雑化に対応
現代のWebアプリケーションでは、パラメータの解決が複雑になっています。
// 複雑なルーティング例
// /shop/[category]/[subcategory]/[product]/reviews/[reviewId]
// 従来:全パラメータの解決を待機
function Page({ params }) {
// category, subcategory, product, reviewId すべて同時に必要
}
// 新仕様:段階的な解決が可能
async function Page({ params }) {
// まずカテゴリレベルの情報を表示
const categoryInfo = await getCategoryInfo();
// 必要になったタイミングでパラメータを解決
const { product, reviewId } = await params;
const productInfo = await getProductInfo(product);
}
実際の移行パターン
パターン1:シンプルな動的ルート
// Before (Next.js 14)
interface Props {
params: { id: string };
}
export default function UserPage({ params }: Props) {
const userId = params.id;
return <div>User: {userId}</div>;
}
// After (Next.js 15)
interface Props {
params: Promise<{ id: string }>;
}
export default async function UserPage({ params }: Props) {
const { id: userId } = await params;
return <div>User: {userId}</div>;
}
パターン2:複数パラメータ + バリデーション
// Before
interface Props {
params: {
category: string;
product: string;
};
}
export default function ProductPage({ params }: Props) {
const { category, product } = params;
if (!category || !product) {
notFound();
}
return <Product category={category} product={product} />;
}
// After
interface Props {
params: Promise<{
category: string;
product: string;
}>;
}
export default async function ProductPage({ params }: Props) {
const { category, product } = await params;
if (!category || !product) {
notFound();
}
return <Product category={category} product={product} />;
}
パターン3:searchParamsと組み合わせ
// Before
interface Props {
params: { id: string };
searchParams: { tab?: string };
}
export default function DetailPage({ params, searchParams }: Props) {
const userId = params.id;
const activeTab = searchParams.tab || 'overview';
return <UserDetail id={userId} tab={activeTab} />;
}
// After
interface Props {
params: Promise<{ id: string }>;
searchParams: Promise<{ tab?: string }>;
}
export default async function DetailPage({ params, searchParams }: Props) {
const { id: userId } = await params;
const { tab } = await searchParams;
const activeTab = tab || 'overview';
return <UserDetail id={userId} tab={activeTab} />;
}
移行時の注意点
1. 型定義の更新
// ❌ よくある間違い
type Props = {
params: {
Promise<{ id: string }>; // 構文エラー
};
}
// ✅ 正しい書き方
type Props = {
params: Promise<{ id: string }>;
}
2. エラーハンドリング
export default async function Page({ params }: Props) {
try {
const { id } = await params;
// パラメータを使用した処理
} catch (error) {
// パラメータの解決に失敗した場合
notFound();
}
}
3. パフォーマンスの考慮
// ❌ 非効率:毎回awaitする
export default async function Page({ params }: Props) {
const { id } = await params;
const data1 = await fetchData1(id);
const { category } = await params; // 再度await(無駄)
const data2 = await fetchData2(category);
}
// ✅ 効率的:一度だけawaitする
export default async function Page({ params }: Props) {
const { id, category } = await params;
const [data1, data2] = await Promise.all([
fetchData1(id),
fetchData2(category)
]);
}
まとめ
Next.js 15のparams
非同期化は、一見面倒な変更に見えますが、以下のメリットがあります:
メリット:
- ストリーミングレンダリングによる高速化
- より柔軟なパフォーマンス最適化
- 複雑なルーティングへの対応
対応方法:
-
params
の型をPromise<>
に変更 - ページコンポーネントを
async
にする -
await params
でパラメータを取得
初回の移行は手間ですが、アプリケーションのパフォーマンスが向上するので、長期的には価値のある変更といえるでしょう。
Discussion