💌

Re: Layout.tsxはミドルウェア的に使ってもいいの?(Next.js 14)

2024/03/04に公開
6

https://zenn.dev/meijin/articles/nextjs14-layout-as-middleware

自分も同じ道を辿ってきて今はちょっと考え方が変わっているので自身の整理がてらmeijinさんの記事に乗っからせていただきます🙏

Re: 「A layout is UI that is shared between routes.」って書いているのでめっちゃめちゃ違う用途じゃないか?

export default async function Layout({ children }: { children: ReactNode }) {
  const supabaseClient = createSupabaseServerComponentClient();
  const { data, error } = await supabaseClient.auth.getUser();

  if (!data.user) {
    // This is unreachable because the user is authenticated
    // But we need to check for it anyway for TypeScript.
    return redirect('/login');
  } else if (error) {
    return <p>Error: An error occurred.</p>;
  }

  return <>{children}</>;
}

UIのようなもの、と広い意味で解釈しています。
なので、未認証の場合はlogin画面のUIを表示するためにredirect()するのも問題なしと捉えてます。

Re: Middlewareの機能部分のみをExportし

middleware.tsでその他の関数をexportするのはナンセンスな気がします。
そもそもmiddlewareはedge runtimeのみサポートされているため、もしそのプロジェクトがnode.js APIを扱うならばそれを含むコードは書けないはず、です。

Re: Runtimeの差

自分もruntimeの解釈を”実行環境”と捉えていました(遠い目)
しかし、Next.jsの文脈ではそうではないようなのです。

https://youtu.be/SEoAqn6r4BE?si=OPjcQndqymBhN36V

https://nextjs.org/docs/app/building-your-application/rendering/edge-and-nodejs-runtimes

In the context of Next.js, runtime refers to the set of libraries, APIs, and general functionality available to your code during execution.
On the server, there are two runtimes where parts of your application code can be rendered:
The Node.js Runtime (default) has access to all Node.js APIs and compatible packages from the ecosystem.
The Edge Runtime is based on Web APIs.

なので、Node.js APIsが使えないだけでどの環境でも使えるはずです。ただし、Web APIsのみ。

そしてDeployのページにも、

https://nextjs.org/docs/app/building-your-application/deploying#middleware

とあるので、間違いないかと思われます。

これは当時deployまわりのアナウンスがあったときに誤解のまま突っ込んで勘違いに気が付き謝罪したツイート
https://twitter.com/t6adev/status/1747047660291559513

Middlewareの認証用途

元記事はこちら
https://pilcrow.vercel.app/blog/clerk-nextjs-vulnerability

冒頭の例であがった認証結果で遷移先を振り分けたい時にmiddlewareでやりたくなる気持ちもすごくわかります。自分もはじめそうすべきだと思っていたし、NextAuth等のよく目にするコードでもそういう実装になっていたりもします。

ただ、pilcrowさんの記事での指摘の通り、middlewareとroute handlerやlayout.tsx間でのデータのやり取りは明確に用意されていません。
(headersを流用すればできなくもない的な事を何処かで見たことがありますが、素直にできない時点でそういうことなんでしょう)

そのため、middlewareで認証チェックして未ログインの場合にredirect()することは可能ですが、ログイン済みのユーザー情報をlayout/pageに渡す方法は無いので結局layout/pageで再度認証周りのコードを書く必要があります。

[追記] layoutの実行順に注意が必要。コメント参照。

[追記] layout.tsxで認証チェックしても、pageに認証後のユーザー情報渡すにはどうするの?

この課題には幸いにもcacheが使えます。cacheはRSCでのみ使えるAPIで、RSCのリクエストを返すまでの間、関数の戻り値をキャッシュする関数を作ってくれます。 ※ リクエスト間でキャッシュするものではないです
https://react.dev/reference/react/cache

auth.ts
import { cache } 'react';

const validateAuthWithRedirect = async () => {
  /* 未認証であればredirect、 認証できればユーザー情報を返す */
};

export const cachedValidateAuthWithRedirect = cache(validateAuthWithRedirect);
layout.tsx
export default async function ProtectedLayout() {
  ...
  await cachedValidateAuthWithRedirect(); 
  ...
}
page.tsx
export default async function ProtectedPage() {
  ...
  // layoutを経由してくるので値はキャッシュされたものを得る
  const user = await cachedValidateAuthWithRedirect(); 
  ...
}

Middlewareについての公式ブログ

まずはv12でのbetaでのアナウンス。認証系もstreaming HTMLも行けるぜ、的な感じです。
https://nextjs.org/blog/next-12#introducing-middleware

次にv12.2でstableになった際のアナウンス。betaで言ったことは触れず、純粋にリクエストに関する処理のみに絞られてます。
https://nextjs.org/blog/next-12#introducing-middleware

v13.1で改良されました。多分ここが現時点(2024.03.04 v14.1)でのMiddlewareに関する最新のアナウンス。
このサンプルコードでは認証チェックしてますが、本題としての改良点はheadersが渡せる様になったことですね。 (experimental.allowMiddlewareResponseBody はもう無い?未確認)
https://nextjs.org/blog/next-13-1#nextjs-advanced-middleware

まとめ

ざっくばらんに書きましたが、現状の私見としては

  • middlewareは純粋なリクエストを処理する場所として扱う、UIにまつわる処理は極力避けるのが妥当
  • layout.tsxで認証チェックしUIを切り替える処理はNext.js的に 正しい 気をつける (※追記参照)
  • [追記] koichik さんのコメント参照: layoutはネストの深い所から順番に実行されるので確実さを求める場合、middleware.tsでの認証チェックしてのリダイレクトは推奨

です。

[追記]Takepepeさんのクオリティで書かれたNext.js本が出ます!

[追記2] まさかの世界のネタと同調してました。
https://twitter.com/sebastienlorber/status/1765405011826098670

そしてこのお言葉。(細かな意見はスレッドを遡ってみてね)
https://twitter.com/sebmarkbage/status/1765415611780186505

なので、これでもlayoutでやる場合は十二分に「気をつける」ことが求められますので、もうやってはいけないと同義ですね(クールクル)

[追記3]
https://twitter.com/koichik/status/1767356855024844809

[追記4]
https://twitter.com/t6adev/status/1767524624693416359

Discussion

koichikkoichik

layout.tsxで認証チェックして未認証ユーザをリダイレクトする場合には注意点があるかと思います

まず、layout.tsxpage.tsxよりも後から実行が開始されます (ただしpage.tsxの完了を待たずにlayout.tsxの実行が開始されます)
また、layout.tsxは内側 (ネストが深い方) から先に実行が開始されます
たとえば/fooにアクセスされた場合、app/foo/page.tsxapp/foo/layout.tsxapp/layout.tsxの順で実行が開始されます
つまり、外側のlayout.tsxで未認証時にredirect()しているからといって、page.tsxや内側のlayout.tsxが実行されないというわけではありません
一方middleware.tsで未認証ユーザの場合にリダイレクトすれば、layout.tsxpage.tsxが実行されることはありません
その場合に認証情報が必要なコンポーネントで

middlewareで認証チェックして未ログインの場合にredirect()することは可能ですが、ログイン済みのユーザー情報をlayout/pageに渡す方法は無いので結局layout/pageで再度認証周りのコードを書く必要があります。

となるのはその通りなのですが、これはApp RouterあるいはRSCでは「コンポーネントが必要な情報はそのコンポーネント自身が取得する」ものなので、そうなるものだと思ってます (それを非効率化しないためにリクエストのメモ化やキャッシュが用意されています)
たとえば間もなく発売されるtakepepeさんの書籍「実践 Next.js —— App Router で進化する Web アプリ開発」のサンプルでは、

としており、参考になるかと思います (なお自分はtakepepeさんの同僚なのでこれはダイマとなります😄)

次に、Next.jsのredirect()はHTTPのステータスコード (307 or 308) を使ったリダイレクトを行うとは限らないことにも注意が必要かと思います
Next.jsはStreaming SSRをサポートしているため、<Suspense>境界 (loading.tsx) があるとその外側は内側のコンポーネントの完了を待たずにレスポンスされることが起こり得ます
そしてその時点でステータスコードが決定してしまいます (通常200)
すると、<Suspense>境界より内側のlayout.tsxredirect()を呼び出したとしても、もはやステータスコード307 or 308を返すことはできないため、redirect()<meta refresh="..." />を使ったHTMLによるリダイレクトにフォールバックします
これにより監視などでリダイレクトを正しく把握できない可能性などが考えられます
middleware.tsであれば確実にHTTPによるリダイレクトができます

Teruhisa - T6ADEVTeruhisa - T6ADEV

layout.tsxは内側 (ネストが深い方) から先に実行が開始されます

これは知りませんでした。。。ありがとうございます!

https://github.com/vercel/next.js/discussions/53026#discussioncomment-6601031

(それにしても仕様に納得はできてない人は多そうですね、自分も含め・・・)

なお自分はtakepepeさんの同僚なので

ずるいです!笑
自分もこの記事を通してtakepepeさんの売上に貢献できますね😂

<Suspense>境界より内側のlayout.tsxがredirect()を呼び出したとしても

これは自分はしないだろうなとは思いつつ、他メンバーがしないとも限らない&自分も100%言い切れないので間違いないですね。

middleware.tsであれば確実にHTTPによるリダイレクトができます

手のひらクルーになりますが、これは確実さを求めるならばその通りですね。

ありがとうございます!

koichikkoichik

(それにしても仕様に納得はできてない人は多そうですね、自分も含め・・・)

実際のところ、実行順そのものに大した意味はないと思うのですよね
仮に親のlayout.tsxから実行が開始されるとしても認証チェックがRedisやDBMSにアクセスするならその非同期処理が完了する前に内側のlayout.tsxpage.tsxの実行が開始されてしまうことになるわけで、おそらく何も解決しませんよね
重要なのはlayout.tsxpage.tsxは並行に実行されることと、それらの実行順に依存関係があってはいけないということかなと思います
layout.tsxpage.tsxの実行順はNext.jsによってたままた内側から開始されるように実装されていることですが、それらが実行順に依存すべきでないというのはReactがComposabilityを重視する考え方と一致しているように思うので、今後もこのままじゃないかなと思ってます

Teruhisa - T6ADEVTeruhisa - T6ADEV

はい、ルーティング機能において、ベストは並列で処理されるべきかなとも思える点はあるのですが、素のReactでネストされたコンポーネントの実行順は親から子になるはずなので直感と反すると思うのです。

自分はReactもNextもドキュメントに書かれている範囲程度の知識で扱っているので、いちユーザーとしてはモヤッとするところ・・・
↑であげたディスカッションを見ても自分以外もそう感じてしまうので、取り敢えず公式から何かしら説得材料は欲しい😅

koichikkoichik

素のReactでネストされたコンポーネントの実行順は親から子になるはずなので直感と反すると思うのです。

素のReactで考えてもApp Routerの場合は内側から実行されるものと理解する方が容易ではないかと思います
というのも、layout.tsxのpropsに渡されるのはchildren、つまり (もし逐次実行されるなら) 内側のlayout.tsxpage.tsxをレンダリングした結果であるReactNodeに見えるだろうと思われるからです
外側から実行されるというメンタルモデルだと、layout.tsxのpropsに渡ってくるchildrenが謎じゃないかなと
ただし、実際のApp Routerではlayout.tsxpage.tsxは並行に実行されるため、layout.tsxに渡されるchildrenReactNodeそのものではなく、それを表すProxyオブジェクトになります (それを前提で考えると外側から実行しているというメンタルモデルでも成立しちゃうんですけどね)

Teruhisa - T6ADEVTeruhisa - T6ADEV

layout.tsxのpropsに渡ってくるchildrenが謎じゃないかな

childrenの中身が謎でもよいのが利点でもありますよね。

外側から実行しているというメンタルモデルでも成立しちゃう

いやぁ、混乱してしまいますね。。。

RSCがasyncコンポーネントとなり、中でawaitできてしまうことでコンポーネントのツリー順に逐次的に実行されてしまうと思いこんでしまうのは許してぇ、という思いです。
最低限ドキュメントにApp Routerの仕様だ、と言い切ってもらえるとまだいいんですけど(今のところDiscussionのleerobの一言だけ?)