🚻

Next.jsのAppRouterでlayout.tsxからServer/Clientコンポーネントにデータを配る

2023/11/27に公開1
  • シリーズ投稿
  1. https://zenn.dev/sora_kumo/articles/approuter-identification
  2. https://zenn.dev/sora_kumo/articles/approuter-cache
  3. https://zenn.dev/sora_kumo/articles/approuter-context

Context を利用できない Server コンポーネント

Next.js の AppRouter の Server コンポーネントでは、createContextが使用できません。これに加え、Next.js ではcreateServerContextも使用できなくなったため、Context を使ってデータをコンポーネントツリーに配ることができません。

PagesRouter 上では_app.tsx から Context を配ることが出来てとても便利でした。しかし AppRouter の Server コンポーネントにはその機能がありません。

そもそも page.tsx が layout.tsx よりも先に実行される

AppRouter のコンポーネント評価順序は以下のようになります。

(1) page.tsx
(2) layout.tsx

つまり、先に page.tsx が実行され、その後 layout.tsx のコンポーネントが評価されます。そのため、layout から page にデータを配ろうにも、データを配る前に page が実行されてしまい、データを配ることができません。

データを配ることも出来なければ、そもそも実行順序も違う

そう、不可能なのです。普通にやったら。

不可能を可能にする

ということで、不可能を可能とするコードを書きました。
npm にパッケージ化したものを登録してあります。

https://www.npmjs.com/package/next-approuter-context

サンプルソース

app/context.ts

Server コンポーネント間で共有するコンテキストを生成します。
Server/Client 間で Context のインスタンスを自動識別する方法がどうしても思いつかなかったので、複数の Context を扱うときはユニークな名前が必要です。

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

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

app/layout.tsx

既存の ContextAPI に似せて Provider を作る書き方にしています。

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 コンポーネントから Context を取得しています。コンポーネントを async にする場合は、データの取得方法が getMixContext を使った形に書き換える必要があります。

"use server";

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

export const Server = () => {
  // If the component is async, it should be written as follows
  // const { text, color } = await getMixContext<ContextType1>();
  const { text, color } = useMixContext(context1);
  const value = useMixContext(context2);
  return (
    <>
      <div style={{ color }}>
        Server: {text} - {value}
      </div>
    </>
  );
};

app/client.tsx

Client コンポーネントから Context を取得しています。基本的に Server コンポーネントと同じコードになるように、ライブラリを作ってあります。

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

実行結果

見事、データが配れない問題と実行順序の問題を解決しました。

色々なことを力技で解決していますが、具体的なソースはこちらを見てください。

https://github.com/ReactLibraries/next-approuter-context/tree/master/src

まとめ

Server/Client の両方に layout.tsx からデータを配れるようになりました。このやり方を使えば、UI ライブラリのテーマ設定も簡単です。

GitHubで編集を提案

Discussion

hohohoho

いい記事ありがとうございます!勉強になりました!
一点だけ質問させてください。
作成して頂いたパッケージを使うことで、layout.tsx からデータを配れるようにはなったが、
逆に下の階層からlayout.tsxのデータを更新することも可能ですか?