Open17

Next.js AppRouter の落とし穴

ハトすけハトすけ

not-found.tsxは内部コンポーネントからnotFound()されたときしか呼ばれない

users/[userId]/not-found.tsxというページを用意したとする。
users/[userId]/page.tsx内部のコンポーネントでnotFound()を呼べば、直近の404ページ、つまりusers/[userId]/not-found.tsxが呼ばれる。

外部からURLを叩いて間違って存在しないページusers/[userId]/nopageにアクセスした場合は、グローバルな404ページ/not-found.tsxが使われるため注意。ちなみにnot-found.tsxはエラーバウンダリの一種であり、ページとしては存在しないので、意図してURLを叩いてそのページに直接飛ぶことはできない。

ハトすけハトすけ

error.tsxにはカスタムエラーを渡せない

以下のようにerror.tsxpage.tsxを実装したとき、error.tsxAPIErrorが渡されることを期待する。しかし、error.tsxmessagedigestプロパティを持ったErrorインスタンスが渡される。

error.tsx
'use client'; // 必須

export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  console.log(error instanceof APIError); // false
  console.log(error.constructor.name); // Error
  console.log(error.statusCode); // undefined statusCodeはAPIErrorのプロパティの1つとする

  return (
    <div>Something went wrong!</div>
  );
}
page.tsx
export default function Page(){
  throw new APIError('message', 400); // カスタムエラー
  return null;
}

セキュリティ上の理由らしい

Next.jsのリファレンス
https://github.com/vercel/next.js/discussions/49506

GitHub上の関連するディスカッション
https://github.com/vercel/next.js/discussions/49506

ハトすけハトすけ

v14.1.*の場合parallel routeで最初のlayoutが適用されない

以下のようなフォルダ構成で、パラレルルートを構成しているばあい、フォルダ先頭のhoge/layoutが適用されない。これは、v14.2.*で修正された

@hoge
  - layout.tsx // これが適用されない
  - page.tsx
@fuga
  - layout.tsx
  - page.tsx
layout.tsx
ハトすけハトすけ

next-auth v4系がApp Routerのmiddelware環境(edge runtime)に対応していない

v5系で対応予定。つまりmiddelware上でnext-authのAPI(getServerSessionとか)を呼べないので直接cookieを取得するといった回避策が必要。

ハトすけハトすけ

parallel route と interception routeを組み合わせたときの挙動が怪しい

v14.2.4で解消されたが、router.refrshrouter.backを組み合わせるとどちらかが上手く発火しないことがある。まだここらへんの挙動は安定していないので、本番で使うのは少し不安。

ハトすけハトすけ

midlewareの edge runtime ではlodash使えないかもしれない

おそらくlodashの関数によってはエラーが発生する。

  • lodash.keyby
  • lodash.isequal
Dynamic Code Evaluation (e. g. 'eval', 'new Function', 'WebAssembly.compile') not allowed in Edge Runtim
ハトすけハトすけ

error.tsxで渡されるエラーメッセージは本番buildすると潰される

すべてのエラーは以下の文章に置き換わる。セキュリティのためらしい。ステータスコードによってエラーを出し分けるのはどうやったらいいのだろう?ステータスコードが200番台以外ならエラーを投げずに、画面に描画するメンタルモデルを採用すべきなのかもしれない。

An error occurred in the Server Components render. The specific message is omitted in production builds to avoid leaking sensitive details. A digest property is included on this error instance which may provide additional details about the nature of the error.

おそらく関連するDiscussion
https://github.com/vercel/next.js/discussions/62681

ハトすけハトすけ

だめな書き方

export default async function Page({
  params: { someId },
}: {
  params: { someId: string };
}) {
  // 200番台以外だとエラー、bodyが直接returnされる
  const some = await fetchSomeOrThrowError(someId);
  return (
    <SomeDescription
      some={some}
    ></SomeDescription>
  );
}

AppRouterの想定する書き方

export default async function Page({
  params: { someId },
}: {
  params: { someId: string };
}) {
  // { ok: boolean, body: Some }が返される
  const res = await fetchSomeOrThrowError(someId);

  if(!res.ok){
    return <div>エラーが発生しました!</div>
  }
  return (
    <SomeDescription
      some={res.body}
    ></SomeDescription>
  );
}
ハトすけハトすけ

nextjsの公式的には想定されるエラーには、ちゃんとpage.tsx内でtry-catch使わずに拾いましょう、と言っている。

https://rc.nextjs.org/docs/app/building-your-application/routing/error-handling#handling-expected-errors-from-server-actions

'use server'
 
import { redirect } from 'next/navigation'
 
export async function createUser(prevState: any, formData: FormData) {
  const res = await fetch('https://...')
  const json = await res.json()
 
  if (!res.ok) {
    return { message: 'Please enter a valid email' }
  }
 
  redirect('/dashboard')
}
ハトすけハトすけ

v14.2.9からexperimental.serverComponentsExternalPackagesに指定したライブラリはクライアントコンポーネントからインポートできない

experimental.serverComponentsExternalPackagesから除外する。しかしなぜこの設定にしたのか覚えていない...

next.config.js
const nextConfig = {
  experimental: {
    serverComponentsExternalPackages: [
      // here
      '@ts-rest/core'
    ],
  },
};

module.exports = nextConfig;
SomeComponent.tsx
'use client';

import { initContract } from '@ts-rest/core'; // here

console.log('initContract', initContract);

export function SomeComponent() {
  const ok = () => {
  };
  return <button onClick={() => ok()}>ok</button>;
}

エラー内容:

 ⨯ Internal error: Error: Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: undefined. You likely forgot to export your component from the file it's defined in, or you might have mixed up default and named imports.
ハトすけハトすけ

TypeError: Response.clone: Body has already been consumed.というエラーが発生する

追記: 2024/12/09

バージョン14.2.14にアップグレードしても同様の現象が確認された。パラレルルーティング内でおなじサーバーアクションにアクセスしてると起こるっぽい。

Issueの中で、14.2.20で修正されたとアナウンスがあった。

Resolved in #73274. Accessible in Next.js 14.2.20 and 15.0.4-canary.40+

https://github.com/vercel/next.js/pull/73274
https://github.com/vercel/next.js/discussions/69635#discussioncomment-11466212


確認したバージョン
nextjs v14.2.8
nodejs v20.?

関連issue:
https://github.com/vercel/next.js/discussions/69635

改善策:
こちらのpatchで修正されたっぽい? next v14.2.14にアップグレードする必要がある
https://github.com/vercel/next.js/pull/70082
#70082がはいったリリースバージョン
https://github.com/vercel/next.js/releases?q=%2370082&expanded=true