Re: Layout.tsxはミドルウェア的に使ってもいいの?(Next.js 14)
自分も同じ道を辿ってきて今はちょっと考え方が変わっているので自身の整理がてら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の文脈ではそうではないようなのです。
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のページにも、
とあるので、間違いないかと思われます。
これは当時deployまわりのアナウンスがあったときに誤解のまま突っ込んで勘違いに気が付き謝罪したツイート
Middlewareの認証用途
元記事はこちら
冒頭の例であがった認証結果で遷移先を振り分けたい時にmiddlewareでやりたくなる気持ちもすごくわかります。自分もはじめそうすべきだと思っていたし、NextAuth等のよく目にするコードでもそういう実装になっていたりもします。
ただ、pilcrowさんの記事での指摘の通り、middlewareとroute handlerやlayout.tsx間でのデータのやり取りは明確に用意されていません。
(headersを流用すればできなくもない的な事を何処かで見たことがありますが、素直にできない時点でそういうことなんでしょう)
そのため、middlewareで認証チェックして未ログインの場合にredirect()することは可能ですが、ログイン済みのユーザー情報をlayout/pageに渡す方法は無いので結局layout/pageで再度認証周りのコードを書く必要があります。
[追記] layoutの実行順に注意が必要。コメント参照。
[追記] layout.tsxで認証チェックしても、pageに認証後のユーザー情報渡すにはどうするの?
この課題には幸いにもcacheが使えます。cacheはRSCでのみ使えるAPIで、RSCのリクエストを返すまでの間、関数の戻り値をキャッシュ
する関数を作ってくれます。 ※ リクエスト間でキャッシュするものではないです
import { cache } 'react';
const validateAuthWithRedirect = async () => {
/* 未認証であればredirect、 認証できればユーザー情報を返す */
};
export const cachedValidateAuthWithRedirect = cache(validateAuthWithRedirect);
export default async function ProtectedLayout() {
...
await cachedValidateAuthWithRedirect();
...
}
export default async function ProtectedPage() {
...
// layoutを経由してくるので値はキャッシュされたものを得る
const user = await cachedValidateAuthWithRedirect();
...
}
Middlewareについての公式ブログ
まずはv12でのbetaでのアナウンス。認証系もstreaming HTMLも行けるぜ、的な感じです。
次にv12.2でstableになった際のアナウンス。betaで言ったことは触れず、純粋にリクエストに関する処理のみに絞られてます。
v13.1で改良されました。多分ここが現時点(2024.03.04 v14.1)でのMiddlewareに関する最新のアナウンス。
このサンプルコードでは認証チェックしてますが、本題としての改良点はheadersが渡せる様になったことですね。 (experimental.allowMiddlewareResponseBody
はもう無い?未確認)
まとめ
ざっくばらんに書きましたが、現状の私見としては
- middlewareは純粋なリクエストを処理する場所として扱う、UIにまつわる処理は極力避けるのが妥当
- layout.tsxで認証チェックしUIを切り替える処理はNext.js的に
正しい気をつける (※追記参照) - [追記] koichik さんのコメント参照: layoutはネストの深い所から順番に実行されるので確実さを求める場合、middleware.tsでの認証チェックしてのリダイレクトは推奨
です。
[追記]Takepepeさんのクオリティで書かれたNext.js本が出ます!
[追記2] まさかの世界のネタと同調してました。
そしてこのお言葉。(細かな意見はスレッドを遡ってみてね)
なので、これでもlayoutでやる場合は十二分に「気をつける」ことが求められますので、もうやってはいけないと同義ですね(クールクル)
[追記3]
[追記4]
Discussion
layout.tsx
で認証チェックして未認証ユーザをリダイレクトする場合には注意点があるかと思いますまず、
layout.tsx
はpage.tsx
よりも後から実行が開始されます (ただしpage.tsx
の完了を待たずにlayout.tsx
の実行が開始されます)また、
layout.tsx
は内側 (ネストが深い方) から先に実行が開始されますたとえば
/foo
にアクセスされた場合、app/foo/page.tsx
、app/foo/layout.tsx
、app/layout.tsx
の順で実行が開始されますつまり、外側の
layout.tsx
で未認証時にredirect()
しているからといって、page.tsx
や内側のlayout.tsx
が実行されないというわけではありません一方
middleware.ts
で未認証ユーザの場合にリダイレクトすれば、layout.tsx
やpage.tsx
が実行されることはありませんその場合に認証情報が必要なコンポーネントで
となるのはその通りなのですが、これはApp RouterあるいはRSCでは「コンポーネントが必要な情報はそのコンポーネント自身が取得する」ものなので、そうなるものだと思ってます (それを非効率化しないためにリクエストのメモ化やキャッシュが用意されています)
たとえば間もなく発売されるtakepepeさんの書籍「実践 Next.js —— App Router で進化する Web アプリ開発」のサンプルでは、
middleware.ts
で認証チェックpage.tsx
で認証情報を取得 (実際は起こり得ないが、もし未認証ならnotFound()
を呼び出す)としており、参考になるかと思います (なお自分はtakepepeさんの同僚なのでこれはダイマとなります😄)
次に、Next.jsの
redirect()
はHTTPのステータスコード (307
or308
) を使ったリダイレクトを行うとは限らないことにも注意が必要かと思いますNext.jsはStreaming SSRをサポートしているため、
<Suspense>
境界 (loading.tsx
) があるとその外側は内側のコンポーネントの完了を待たずにレスポンスされることが起こり得ますそしてその時点でステータスコードが決定してしまいます (通常
200
)すると、
<Suspense>
境界より内側のlayout.tsx
がredirect()
を呼び出したとしても、もはやステータスコード307
or308
を返すことはできないため、redirect()
は<meta refresh="..." />
を使ったHTMLによるリダイレクトにフォールバックしますこれにより監視などでリダイレクトを正しく把握できない可能性などが考えられます
middleware.ts
であれば確実にHTTPによるリダイレクトができますこれは知りませんでした。。。ありがとうございます!
(それにしても仕様に納得はできてない人は多そうですね、自分も含め・・・)
ずるいです!笑
自分もこの記事を通してtakepepeさんの売上に貢献できますね😂
これは自分はしないだろうなとは思いつつ、他メンバーがしないとも限らない&自分も100%言い切れないので間違いないですね。
手のひらクルーになりますが、これは確実さを求めるならばその通りですね。
ありがとうございます!
実際のところ、実行順そのものに大した意味はないと思うのですよね
仮に親の
layout.tsx
から実行が開始されるとしても認証チェックがRedisやDBMSにアクセスするならその非同期処理が完了する前に内側のlayout.tsx
やpage.tsx
の実行が開始されてしまうことになるわけで、おそらく何も解決しませんよね重要なのは
layout.tsx
やpage.tsx
は並行に実行されることと、それらの実行順に依存関係があってはいけないということかなと思いますlayout.tsx
やpage.tsx
の実行順はNext.jsによってたままた内側から開始されるように実装されていることですが、それらが実行順に依存すべきでないというのはReactがComposabilityを重視する考え方と一致しているように思うので、今後もこのままじゃないかなと思ってますはい、ルーティング機能において、ベストは並列で処理されるべきかなとも思える点はあるのですが、素のReactでネストされたコンポーネントの実行順は親から子になるはずなので直感と反すると思うのです。
自分はReactもNextもドキュメントに書かれている範囲程度の知識で扱っているので、いちユーザーとしてはモヤッとするところ・・・
↑であげたディスカッションを見ても自分以外もそう感じてしまうので、取り敢えず公式から何かしら説得材料は欲しい😅
素のReactで考えてもApp Routerの場合は内側から実行されるものと理解する方が容易ではないかと思います
というのも、
layout.tsx
のpropsに渡されるのはchildren
、つまり (もし逐次実行されるなら) 内側のlayout.tsx
やpage.tsx
をレンダリングした結果であるReactNode
に見えるだろうと思われるからです外側から実行されるというメンタルモデルだと、
layout.tsx
のpropsに渡ってくるchildren
が謎じゃないかなとただし、実際のApp Routerでは
layout.tsx
やpage.tsx
は並行に実行されるため、layout.tsx
に渡されるchildren
はReactNode
そのものではなく、それを表すProxy
オブジェクトになります (それを前提で考えると外側から実行しているというメンタルモデルでも成立しちゃうんですけどね)childrenの中身が謎でもよいのが利点でもありますよね。
いやぁ、混乱してしまいますね。。。
RSCがasyncコンポーネントとなり、中でawaitできてしまうことでコンポーネントのツリー順に逐次的に実行されてしまうと思いこんでしまうのは許してぇ、という思いです。
最低限ドキュメントにApp Routerの仕様だ、と言い切ってもらえるとまだいいんですけど(今のところDiscussionのleerobの一言だけ?)