😸

Next.jsのAppRouterでlayout.tsxを先に実行しpage.tsxにデータを配る(14系統版)

2024/03/11に公開2

実行順序

昨今、AppRouter で page.tsx が layout.tsx より先に実行されることが話題になっていますが、去年こんな記事を書きました。

https://zenn.dev/sora_kumo/articles/approuter-context

箸にも棒にも引っかからなかったこの記事ですが、次元を歪めることに成功し実行順序を変えています。しかし久々に実行してみると Next.js の仕様が変わって、"use server"したファイルが非同期関数しかエクスポートできないという仕様に変わっており、修正が必要になっていたので、焼き直しで記事を書くことにしました。

layout.tsx を page.tsx より先に実行する

ということでやってみます。

Sample コードと実行環境はこちら

app/context.tsx

React の ContextAPI 風にコンテキストを作ります。

import { createMixContext } from "next-approuter-context";

export const context1 = createMixContext<{ text: string; color: string }>(
  "context1"
);
export const context2 = createMixContext<number>("context2");

app/layout.tsx

レイアウトにデータを設置します。当然のごとく、page.tsx よりも layout.tsx が先に実行が完了されないとデータを配れません。

import { context1, context2 } from "./context";

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        <context1.Provider
          value={{ text: "Send colors and text from Layout", color: "red" }}
        >
          <context2.Provider value={123456}>{children}</context2.Provider>
        </context1.Provider>
      </body>
    </html>
  );
}

app/page.tsx

Server/Client の両方のコンポーネントを呼び出します。

import { Client } from "./client";
import { Server } from "./server";

const Page = () => {
  return (
    <>
      <Server />
      <Client />
    </>
  );
};

export default Page;

app/server.tsx

Server component で、layout.tsx からのデータを受け取ります。

"use server";

import { context1, context2 } from "./context";
import { getMixContext } from "next-approuter-context";

export const Server = async () => {
  const { text, color } = await getMixContext(context1);
  const value = await getMixContext(context2);
  return (
    <>
      <div style={{ color }}>
        Server: {text} - {value}
      </div>
    </>
  );
};

app/client.tsx

Client component で、layout.tsx からのデータを受け取ります。

"use client";

import { useMixContext } from "next-approuter-context";
import { context1, context2 } from "./context";

export const Client = () => {
  const { text, color } = useMixContext(context1);
  const value = useMixContext(context2);
  return (
    <>
      <div style={{ color }}>
        Client: {text} - {value}
      </div>
    </>
  );
};

実行結果

まとめ

AppRouter で layout.tsx の実行を先に完了させ、さらに page.tsx へデータを配ることに成功しました。React の renderToReadableStream を使っている Next.js や Remix の場合、コンポーネントの処理中に保留と再開が任意に可能です。つまりサーバ側のレンダリング処理でそのタイミングを制御するのは、やろうと思えばいくらでも可能なのです。今回も、単にデータ配布の非同期解決まで page.tsx を待たせているだけで、大したことはしていません。

まずは、それが可能だと思うことが重要です。

GitHubで編集を提案

Discussion

koichikkoichik

昨今、AppRouter で page.tsx が layout.tsx より先に実行されることが話題になっていますが、

その発端となったのは 自分のコメント だと思うのですが、そこで自分が書いたのは

まず、layout.tsxpage.tsxよりも後から実行が開始されます

ということで実行の「開始順」でした
しかし、こちらの記事では

AppRouter で layout.tsx の実行を先に完了させ、

と書いてあるように実行の「完了順」を変えたという話ですね(より細かくいうとpage.tsxlayout.tsxの間で同期するという話ですね)
それがダメとか無価値とかいうことでは全然ないのですが、ちょっと話がズラされてるというか別の話をされてる感があります(そしてそれが明確ではない)

また、発端となった記事の主題は認証チェックであり、元々そこでやってほしかったことは「layout.tsxで未認証ユーザをリダイレクトしたならpage.tsxは実行されない」ことであったと思います
なので自分は上と同じコメントで

つまり、外側のlayout.tsxで未認証時にredirect()しているからといって、page.tsxや内側のlayout.tsxが実行されないというわけではありません

とも書いたわけですが、こちらの記事で紹介されている方法でそれを解決できるわけではありませんね(だから価値がないという話ではありません)
実際はちょっとした高階関数でpage.tsx等のコンポーネントをラップするだけで「そう見える」ようにするのはとても簡単なのですが、それをすべきだとは思いません
発端となった記事への別コメント

重要なのはlayout.tsxpage.tsxは並行に実行されることと、それらの実行順に依存関係があってはいけないということかなと思います

と書いたように、そもそも実行の順序に依存するのがApp Routerあるいはcomposabilityを重視するReact的な考え方から外れていると思うからです

本記事の締めである

まずは、それが可能だと思うことが重要です。

を否定するわけではありませんが、同時に「それをすべきか」と考えることも重要ではないかなと思いました(余計なお世話なら申し訳ありません)

koichikkoichik

こちらの記事で紹介されているサンプルアプリを動かしたわけでも、そこで使われているnext-approuter-contextのコードをじっくり読んだわけでもないので勘違いしているかもしれませんが、これって<Link>を使ったソフトナビゲーションでもちゃんと動きますか?
(ちゃんと動くように使うのが大変じゃありませんか?の方が適切な質問かも)

App Routerではlayout.tsxが常に実行されるとは限らず、ソフトナビゲーションでは遷移元と遷移先で共通のセグメントに含まれるlayout.tsxは実行されません
たとえば本記事のサンプルアプリにapp/foo/page.tsxを追加して、ブラウザで/fooから/<Link>で遷移するとapp/layout.tsxは実行されず、app/page.tsxだけが実行されるはずです(app/page.tsxが動的レンダリングなrouteの場合)
するとapp/page.tsxPromiseの完了を永遠に待ち続けたりしませんか?

そうはならないかもしれないし(ちゃんと実装を見たわけではないので)、そうなったとしても対策をすることは「可能だと思う」かもしれませんが、そもそも実行順に依存するという高い結合度を導入しない方がずっといいんじゃないかなと思ってしまいます