🦔

# 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);
  // ...
}

主な変更点:

  • paramsPromise型になった
  • ページコンポーネントを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非同期化は、一見面倒な変更に見えますが、以下のメリットがあります:

メリット:

  • ストリーミングレンダリングによる高速化
  • より柔軟なパフォーマンス最適化
  • 複雑なルーティングへの対応

対応方法:

  1. paramsの型をPromise<>に変更
  2. ページコンポーネントをasyncにする
  3. await paramsでパラメータを取得

初回の移行は手間ですが、アプリケーションのパフォーマンスが向上するので、長期的には価値のある変更といえるでしょう。

参考リンク

Discussion