Open54

ReactのSerialize

mugimugi

とりあえずシリアライズ関数はこのあたり

https://github.com/facebook/react/blob/ed3c65caf042f75fe2fdc2a5e568a9624c6175fb/packages/react-server/src/ReactFlightServer.js#L1388-L1396

renderFunctionComponent とか renderElement から呼ばれる

https://github.com/facebook/react/blob/ed3c65caf042f75fe2fdc2a5e568a9624c6175fb/packages/react-server/src/ReactFlightServer.js#L595-L603
https://github.com/facebook/react/blob/ed3c65caf042f75fe2fdc2a5e568a9624c6175fb/packages/react-server/src/ReactFlightServer.js#L683-L686

renderModelDestructive

renderModelDestructive( renderFunctionComponent, renderElement, renderModel とかからも呼ばれる)

retryTask

performWork

startWork

renderToReadableStream (Next.js側)

mugimugi

renderModelDestructiveを再帰的に呼び出してシリアライズしてる、という感じぽい

mugimugi

↑をtask.toJSONでシリアライズ
https://github.com/facebook/react/blob/ed3c65caf042f75fe2fdc2a5e568a9624c6175fb/packages/react-server/src/ReactFlightServer.js#L2438-L2444

toJSONは、JSON.stringifyの第二引数(replacer)に渡される
https://github.com/facebook/react/blob/ed3c65caf042f75fe2fdc2a5e568a9624c6175fb/packages/react-server/src/ReactFlightServer.js#L210

つまり、JSON.stringifyによって勝手に要素を辿って関数を適用していくことになる

mugimugi

Streamを考える。

たとえば次のようなもの

2:"$Sreact.suspense"
4:I["(app-pages-browser)/./src/app/Client.tsx",["app/page","static/chunks/app/page.js"],"Client"]
5:"$SSYMBOL"
a:I["(app-pages-browser)/./node_modules/next/dist/client/components/layout-router.js",["app-pages-internals","static/chunks/app-pages-internals.js"],""]
b:I["(app-pages-browser)/./node_modules/next/dist/client/components/render-from-template-context.js",["app-pages-internals","static/chunks/app-pages-internals.js"],""]
3:{"id":"4450fa3df088dad8db026c6c81f2c0bb592158f7","bound":null}
6:[["mapKey1","mapValue1"],["mapKey2","mapValue2"],["mapKey3","mapValue3"]]
7:["set1","set2","set3"]
8:{"id":"7de9e92d780a000fa18feb74a7ec9a83a73ddc8f","bound":null}
0:["development",[[["",{"children":["__PAGE__",{}]},"$undefined","$undefined",true],["",{"children":["__PAGE__",{},["$L1",["$","main",null,{"children":[["$","h1",null,{"children":"Page"}],["$","$2",null,{"children":[["$","form",null,{"action":"$F3","children":["$","button",null,{"type":"submit","children":"Submit"}]}],["$","$L4",null,{"string":"abc","number":123,"boolean":true,"undefined":"$undefined","null":null,"symbol":"$5","iter_string":["a","b","c"],"iter_array":["a","b","c"],"iter_map":"$Q6","iter_set":"$W7","iter_typed_array":[1,2,3],"date":"$D2024-04-13T08:10:15.385Z","object":{"key1":"key1value","key2":{"key3":12344,"key4":false}},"jsx":["$","span",null,{"children":"JSX"}],"action":"$F8","promise":"$@9"}]]}]]}],null]]},[null,["$","html",null,{"children":["$","body",null,{"children":["$","$La",null,{"parallelRouterKey":"children","segmentPath":["children"],"loading":"$undefined","loadingStyles":"$undefined","loadingScripts":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$Lb",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[],"styles":null}]}]}],null]],[null,"$Lc"]]]]
c:[["$","meta","0",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","meta","1",{"charSet":"utf-8"}],["$","link","2",{"rel":"icon","href":"/favicon.ico","type":"image/x-icon","sizes":"16x16"}]]
1:null
9:"RESOLVED!!"

これらは、ReactFlightServer 内の emitXXX 系の関数とかを見るとよい

https://github.com/facebook/react/blob/ed3c65caf042f75fe2fdc2a5e568a9624c6175fb/packages/react-server/src/ReactFlightServer.js#L2450
https://github.com/facebook/react/blob/ed3c65caf042f75fe2fdc2a5e568a9624c6175fb/packages/react-server/src/ReactFlightServer.js#L1989-L1994

先頭の 0: とか a: とかは、個々のタスク(モデルとか、importとか)に応じた連番を16進数化したものが付与されてる。

: の後ろにはタグが付与されることがある。
4:I["(app-pages-browser)/./src..... みたいなものだと、l が付与されてる。
この場合は、Import系のチャンクという扱いになる。

他にも次のような種類がある

  • P : Postpone Chunk
  • E : Error Chunk
  • H : Hint Chunk
  • D : Debug Chunk
  • W : Console Chunk
mugimugi

https://ja.react.dev/reference/react/use-client
に記述のあるシリアライズ周り

  • プリミティブ
    • 文字列
    • 数値
    • bigint
    • ブーリアン
    • undefined
    • null
    • シンボル、ただし Symbol.for を通じてグローバルシンボルレジストリに登録されたシンボルのみ
  • シリアライズ可能な値を含んだ Iterable
    • 文字列
    • 配列
    • Map
    • Set
    • TypedArray と ArrayBuffer
  • Date
  • プレーンなオブジェクト: オブジェクト初期化子で作成され、シリアライズ可能なプロパティを持つもの
  • サーバアクション (server action) としての関数
  • クライアントまたはサーバコンポーネントの要素(JSX)
  • プロミス

以下がサポートされない

  • クライアントとマークされたモジュールからエクスポートされていない、または 'use server' でマークされていない関数
  • クラス
  • 任意のクラスのインスタンス(上記の組み込みクラスを除く)や、null プロトタイプのオブジェクト
  • グローバルに登録されていないシンボル、例:Symbol('my new symbol')
mugimugi

実際の挙動からみてみる

mugimugi

Server Components

page.tsx
import { Suspense } from "react";
import { Client } from "./Client";

export const revalidate = 0;

async function serverAction() {
  "use server";
  await Promise.resolve();
}

export default function Page() {
  const promise = new Promise((resolve) => setTimeout(resolve, 1000));

  return (
    <main>
      <h1>Page</h1>

      <Suspense>
        <Client
          string="abc"
          number={123}
          // bigint={BigInt(9999999999999999999)}
          boolean={true}
          undefined={undefined}
          null={null}
          symbol={Symbol.for("SYMBOL")}
          iter_string={"abc"[Symbol.iterator]()}
          iter_array={["a", "b", "c"]}
          iter_map={(() => {
            const map = new Map();
            map.set("mapKey1", "mapValue1");
            map.set("mapKey2", "mapValue2");
            map.set("mapKey3", "mapValue3");
            return map;
          })()}
          iter_set={new Set(["set1", "set2", "set3"])}
          iter_typed_array={new Int8Array([1, 2, 3])}
          date={new Date()}
          object={{ key1: "key1value", key2: { key3: 12344, key4: false } }}
          jsx={<span>JSX</span>}
          action={serverAction}
          promise={promise}
        />
      </Suspense>
    </main>
  );
}

Client Component

Client.tsx
"use client";

import { useRouter } from "next/navigation";
import { use } from "react";

type Props = {
  string: string;
  number: number;
  // bigint: BigInt;
  boolean: boolean;
  undefined: undefined;
  null: null;
  symbol: Symbol;
  iter_string: Iterator<string>;
  iter_array: Array<any>;
  iter_map: Map<any, any>;
  iter_set: Set<any>;
  iter_typed_array: Int8Array;
  date: Date;
  object: { key1: string; key2: { key3: number; key4: boolean } };
  jsx: JSX.Element;
  action: () => Promise<void>;
  promise: Promise<any>;
};

export function Client(props: Props) {
  use(props.promise);

  const router = useRouter();

  return (
    <section>
      <h1>Client</h1>
      <button type="button" onClick={() => router.refresh()}>
        Refresh
      </button>
    </section>
  );
}
mugimugi

Refreshボタンを押すことで router.refresh が発火して、こんな感じのデータが得られる(整形済み)

2:"$Sreact.suspense"
4:I["(app-pages-browser)/./src/app/Client.tsx",["app/page","static/chunks/app/page.js"],"Client"]
5:"$SSYMBOL"
a:I["(app-pages-browser)/./node_modules/next/dist/client/components/layout-router.js",["app-pages-internals","static/chunks/app-pages-internals.js"],""]
b:I["(app-pages-browser)/./node_modules/next/dist/client/components/render-from-template-context.js",["app-pages-internals","static/chunks/app-pages-internals.js"],""]
3:{"id":"4450fa3df088dad8db026c6c81f2c0bb592158f7","bound":null}
6:[["mapKey1","mapValue1"],["mapKey2","mapValue2"],["mapKey3","mapValue3"]]
7:["set1","set2","set3"]
8:{"id":"7de9e92d780a000fa18feb74a7ec9a83a73ddc8f","bound":null}
0:["development",[[["",{"children":["__PAGE__",{}]},"$undefined","$undefined",true],["",{"children":["__PAGE__",{},["$L1",["$","main",null,{"children":[["$","h1",null,{"children":"Page"}],["$","$2",null,{"children":[["$","form",null,{"action":"$F3","children":["$","button",null,{"type":"submit","children":"Submit"}]}],["$","$L4",null,{"string":"abc","number":123,"boolean":true,"undefined":"$undefined","null":null,"symbol":"$5","iter_string":["a","b","c"],"iter_array":["a","b","c"],"iter_map":"$Q6","iter_set":"$W7","iter_typed_array":[1,2,3],"date":"$D2024-04-13T08:10:15.385Z","object":{"key1":"key1value","key2":{"key3":12344,"key4":false}},"jsx":["$","span",null,{"children":"JSX"}],"action":"$F8","promise":"$@9"}]]}]]}],null]]},[null,["$","html",null,{"children":["$","body",null,{"children":["$","$La",null,{"parallelRouterKey":"children","segmentPath":["children"],"loading":"$undefined","loadingStyles":"$undefined","loadingScripts":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$Lb",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[],"styles":null}]}]}],null]],[null,"$Lc"]]]]
c:[["$","meta","0",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","meta","1",{"charSet":"utf-8"}],["$","link","2",{"rel":"icon","href":"/favicon.ico","type":"image/x-icon","sizes":"16x16"}]]
1:null
9:"RESOLVED!!"
[
  "development",
  [
    [
      ["", { "children": ["__PAGE__", {}] }, "$undefined", "$undefined", true],
      [
        "",
        {
          "children": [
            "__PAGE__",
            {},
            [
              "$L1",
              [
                "$",
                "main",
                null,
                {
                  "children": [
                    ["$", "h1", null, { "children": "Page" }],
                    [
                      "$",
                      "$2",
                      null,
                      {
                        "children": [
                          [
                            "$",
                            "form",
                            null,
                            {
                              "action": "$F3",
                              "children": [
                                "$",
                                "button",
                                null,
                                { "type": "submit", "children": "Submit" }
                              ]
                            }
                          ],
                          [
                            "$",
                            "$L4",
                            null,
                            {
                              "string": "abc",
                              "number": 123,
                              "boolean": true,
                              "undefined": "$undefined",
                              "null": null,
                              "symbol": "$5",
                              "iter_string": ["a", "b", "c"],
                              "iter_array": ["a", "b", "c"],
                              "iter_map": "$Q6",
                              "iter_set": "$W7",
                              "iter_typed_array": [1, 2, 3],
                              "date": "$D2024-04-13T08:10:15.385Z",
                              "object": {
                                "key1": "key1value",
                                "key2": { "key3": 12344, "key4": false }
                              },
                              "jsx": ["$", "span", null, { "children": "JSX" }],
                              "action": "$F8",
                              "promise": "$@9"
                            }
                          ]
                        ]
                      }
                    ]
                  ]
                }
              ],
              null
            ]
          ]
        },
        [
          null,
          [
            "$",
            "html",
            null,
            {
              "children": [
                "$",
                "body",
                null,
                {
                  "children": [
                    "$",
                    "$La",
                    null,
                    {
                      "parallelRouterKey": "children",
                      "segmentPath": ["children"],
                      "loading": "$undefined",
                      "loadingStyles": "$undefined",
                      "loadingScripts": "$undefined",
                      "hasLoading": false,
                      "error": "$undefined",
                      "errorStyles": "$undefined",
                      "errorScripts": "$undefined",
                      "template": ["$", "$Lb", null, {}],
                      "templateStyles": "$undefined",
                      "templateScripts": "$undefined",
                      "notFound": [
                        [
                          "$",
                          "title",
                          null,
                          { "children": "404: This page could not be found." }
                        ],
                        [
                          "$",
                          "div",
                          null,
                          {
                            "style": {
                              "fontFamily": "system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"",
                              "height": "100vh",
                              "textAlign": "center",
                              "display": "flex",
                              "flexDirection": "column",
                              "alignItems": "center",
                              "justifyContent": "center"
                            },
                            "children": [
                              "$",
                              "div",
                              null,
                              {
                                "children": [
                                  [
                                    "$",
                                    "style",
                                    null,
                                    {
                                      "dangerouslySetInnerHTML": {
                                        "__html": "body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"
                                      }
                                    }
                                  ],
                                  [
                                    "$",
                                    "h1",
                                    null,
                                    {
                                      "className": "next-error-h1",
                                      "style": {
                                        "display": "inline-block",
                                        "margin": "0 20px 0 0",
                                        "padding": "0 23px 0 0",
                                        "fontSize": 24,
                                        "fontWeight": 500,
                                        "verticalAlign": "top",
                                        "lineHeight": "49px"
                                      },
                                      "children": "404"
                                    }
                                  ],
                                  [
                                    "$",
                                    "div",
                                    null,
                                    {
                                      "style": { "display": "inline-block" },
                                      "children": [
                                        "$",
                                        "h2",
                                        null,
                                        {
                                          "style": {
                                            "fontSize": 14,
                                            "fontWeight": 400,
                                            "lineHeight": "49px",
                                            "margin": 0
                                          },
                                          "children": "This page could not be found."
                                        }
                                      ]
                                    }
                                  ]
                                ]
                              }
                            ]
                          }
                        ]
                      ],
                      "notFoundStyles": [],
                      "styles": null
                    }
                  ]
                }
              ]
            }
          ],
          null
        ]
      ],
      [null, "$Lc"]
    ]
  ]
]
mugimugi

各種シリアライズの詳細

mugimugi

文字列 (および Date)

https://github.com/facebook/react/blob/ed3c65caf042f75fe2fdc2a5e568a9624c6175fb/packages/react-server/src/ReactFlightServer.js#L1712-L1735

  • Taint API によるチェック
  • 末尾が Z の場合かつ parent[parentPropertyName] が Date なら Dateの文字列として扱う
  • 大きい文字列は別チャンクにする
  • 先頭が $ の場合は $$ にエスケープする


Dateの場合はDate自体が持つ toJSON() の実行後の値がreplacerに渡されるため、 "2024-04-13T05:34:21.966Z"のような文字列になる
https://github.com/facebook/react/blob/ed3c65caf042f75fe2fdc2a5e568a9624c6175fb/packages/react-server/src/ReactFlightServer.js#L1040-L1044

mugimugi
mugimugi

Array および Iterable

Array
https://github.com/facebook/react/blob/ed3c65caf042f75fe2fdc2a5e568a9624c6175fb/packages/react-server/src/ReactFlightServer.js#L1592-L1594

Iterable
https://github.com/facebook/react/blob/ed3c65caf042f75fe2fdc2a5e568a9624c6175fb/packages/react-server/src/ReactFlightServer.js#L1665-L1668

https://github.com/facebook/react/blob/ed3c65caf042f75fe2fdc2a5e568a9624c6175fb/packages/react-server/src/ReactFlightServer.js#L689-L746

  • Fragmentの場合を考慮したシリアライズが入る (task.keyPath で判断している)
  • Fragmentじゃない場合はそのまま返す(JSON.stringifyのreplacerによって配列内の要素は再度呼び出される)
mugimugi
mugimugi

Promise

https://github.com/facebook/react/blob/ed3c65caf042f75fe2fdc2a5e568a9624c6175fb/packages/react-server/src/ReactFlightServer.js#L1546-L1571

https://github.com/facebook/react/blob/ed3c65caf042f75fe2fdc2a5e568a9624c6175fb/packages/react-server/src/ReactFlightServer.js#L417-L520

https://github.com/facebook/react/blob/ed3c65caf042f75fe2fdc2a5e568a9624c6175fb/packages/react-server/src/ReactFlightServer.js#L1002-L1004

  • createTask() で ID を得る。
  • IDをもとに '$@' + ID とする

JSON内が "promise":"$@8" だとして、Promiseを "RESOLVED!!" といった文字列でresolveした場合、 Stream に 8:"RESOLVED!!" が来る

  thenable.then(
    value => {
      newTask.model = value;
      pingTask(request, newTask);
    },
    reason => {
      if (
        enablePostpone &&
        typeof reason === 'object' &&
        reason !== null &&
        (reason: any).$$typeof === REACT_POSTPONE_TYPE
      ) {
        const postponeInstance: Postpone = (reason: any);
        logPostpone(request, postponeInstance.message);
        emitPostponeChunk(request, newTask.id, postponeInstance);
      } else {
        newTask.status = ERRORED;
        const digest = logRecoverableError(request, reason);
        emitErrorChunk(request, newTask.id, digest, reason);
      }
      request.abortableTasks.delete(newTask);
      if (request.destination !== null) {
        flushCompletedChunks(request, request.destination);
      }
    },
  );

  return newTask.id;

postpone is 何

関連しそうなPR
https://github.com/facebook/react/pull/27238

  • unstable_postpone() (※実態は packages/react/src/ReactPostpone.js # postpone())を呼ぶことで、 REACT_POSTPONE_TYPE がマークされたErrorがthrowされる
  • Promiseがrejectされた理由が、この REACT_POSTPONE_TYPE のエラーが理由だった場合だけ特別な扱いにしている
    • 解決されない無限のPromiseで使う、みたいな話らしい

Client側は "P" のフラグを見てこのあたりでさばいてそう
https://github.com/facebook/react/blob/ed3c65caf042f75fe2fdc2a5e568a9624c6175fb/packages/react-client/src/ReactFlightClient.js#L1365-L1379

ただ、現状 enablePostpone が false なのでNextだとあんまり関係ない
普通に使ってた。

あっきーさんに教えてもらったが、PPR絡みの模様
https://x.com/akfm_sato/status/1779061182370062697

mugimugi

クライアント側のメモ

mugimugi
mugimugi
mugimugi

ROW_ID のときの処理

IDを探し中の処理

https://github.com/facebook/react/blob/ed3c65caf042f75fe2fdc2a5e568a9624c6175fb/packages/react-client/src/ReactFlightClient.js#L1404-L1413

  • ":" が見つかるまで探す
  • そこまでに出てきたものは rowId として記録する
  • 16進数文字列なので、それをビット演算して10進数のNumberに戻してる

e.g.)

"1a" が来た場合、

  1. "1" の charCode は 49 なので、49 - 48 = 1 を一時的に rowId に設定
  2. 現時点での rowId を 4ビット(16進1桁の範囲)を左にシフト ※Aとする
  3. "a" の charCode は97 なので、97 - 87 = 10 を取得。Aと論理和を取る → 26 になる

: が見つかったら、ステータスを ROW_TAG に切り替え

mugimugi

ROW_TAG のときの処理

https://github.com/facebook/react/blob/ed3c65caf042f75fe2fdc2a5e568a9624c6175fb/packages/react-client/src/ReactFlightClient.js#L1414-L1446

  • : の次の1文字を見る
  • "T" または enableBinaryFlight が true で "A", "C", "c", "U", "S", "s", "L", "l", "F", "d", "N", "m", "V" のとき
    • rowTag に取得した1文字を設定
    • rowState を ROW_LENGTH に切り替え
  • ↑に該当せず、"A" - "Z" のとき
    • rowTag に取得した1文字を設定
    • rowState を ROW_CHUNK_BY_NEWLINE に切り替え
  • ↑の両方に該当しないとき
    • rowTag を 0 に設定
    • rowState を ROW_CHUNK_BY_NEWLINE に切り替え

まとめると、ROW_LENGTHまたはROW_CHUNK_BY_NEWLINEに切り替える判断をしてる感じぽい

mugimugi

ROW_LENGTH のときの処理

https://github.com/facebook/react/blob/ed3c65caf042f75fe2fdc2a5e568a9624c6175fb/packages/react-client/src/ReactFlightClient.js#L1447-L1456

  • "," が見つかるまでの文字列を、16進数から10進数値に変換してる(IDの処理と近い)
    • これを rowLength に設定
  • "," が見つかったら、rowState を ROW_CHUNK_BY_LENGTH に切り替え

参考:
Server側で、TypedArray などの場合は、タグの後ろに16進数文字列でサイズ情報が入ってくるぽい。

https://github.com/facebook/react/blob/ed3c65caf042f75fe2fdc2a5e568a9624c6175fb/packages/react-server/src/ReactFlightServer.js#L1608-L1615
https://github.com/facebook/react/blob/ed3c65caf042f75fe2fdc2a5e568a9624c6175fb/packages/react-server/src/ReactFlightServer.js#L1231-L1235

ROW_CHUNK_BY_LENGTH

https://github.com/facebook/react/blob/ed3c65caf042f75fe2fdc2a5e568a9624c6175fb/packages/react-client/src/ReactFlightClient.js#L1462-L1469

  • ROW_LENGTH 時の処理で得た rowLength をもとに、チャンクの終了位置を lastIdx に保持
mugimugi

Client - チャンク本体の Deserialize

https://github.com/facebook/react/blob/ed3c65caf042f75fe2fdc2a5e568a9624c6175fb/packages/react-client/src/ReactFlightClient.js#L1252-L1259

先のチャンク解析で得たrowTag(この関数内では tag)によって処理を分岐していく。

つまりは、チャンクが 4:I["(app-pages-browser)/./src/app/Client.tsx",... であれば、先頭の 4:I の 4 が id で I が tag

mugimugi

"I"

https://github.com/facebook/react/blob/ed3c65caf042f75fe2fdc2a5e568a9624c6175fb/packages/react-client/src/ReactFlightClient.js#L1315-L1318

  • おそらく Import の I
  • requireAsyncModule() 経由でモジュールをimport
  • webpackの場合だと、__webpack_require__ が実行される

https://github.com/facebook/react/blob/ed3c65caf042f75fe2fdc2a5e568a9624c6175fb/packages/react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpack.js#L173-L207

4:I["(app-pages-browser)/./src/app/Client.tsx",["app/page","static/chunks/app/page.js"],"Client"]

とかだと、 ["(app-pages-browser)/./src/app/Client.tsx",["app/page","static/chunks/app/page.js"],"Client"] の部分を parseJson する。

↓のような型になる想定らしい
https://github.com/facebook/react/blob/ed3c65caf042f75fe2fdc2a5e568a9624c6175fb/packages/react-server-dom-webpack/src/shared/ReactFlightImportMetadata.js#L19-L31

mugimugi

"H"

https://github.com/facebook/react/blob/ed3c65caf042f75fe2fdc2a5e568a9624c6175fb/packages/react-client/src/ReactFlightClient.js#L1319-L1323

  • HintのHらしい
  • Server側では emitHint() でチャンクが生成されるが、 packages/react-dom-bindings/src/server/ReactDOMFlightServerHostDispatcher.js からしか呼び出しが無い
  • Float (https://github.com/facebook/react/pull/25243 ) 関連
    • flushSyncWork prefetchDNS preconnect preload preloadModule preinitScript preinitStyle preinitModuleScript とかが該当する

このあたりから、Floatのメソッドが呼ばれるぽい https://github.com/facebook/react/blob/ed3c65caf042f75fe2fdc2a5e568a9624c6175fb/packages/react-client/src/ReactFlightClient.js#L1150-L1157
https://github.com/facebook/react/blob/ed3c65caf042f75fe2fdc2a5e568a9624c6175fb/packages/react-dom-bindings/src/shared/ReactFlightClientConfigDOM.js#L19-L23

mugimugi
mugimugi
mugimugi
mugimugi

"W"

https://github.com/facebook/react/blob/ed3c65caf042f75fe2fdc2a5e568a9624c6175fb/packages/react-client/src/ReactFlightClient.js#L1354-L1364

  • 何の W かはわからないが、とりあえずDev時しか送られてこない想定らしい
  • consoleに出力する

https://github.com/facebook/react/blob/ed3c65caf042f75fe2fdc2a5e568a9624c6175fb/packages/react-client/src/ReactFlightClientConsoleConfigBrowser.js#L23-L28

このPRで入ったぽい
https://github.com/facebook/react/pull/28275

どうやら、現在は experimental な enableServerComponentLogs フラグが有効化されると、サーバサイドでのconsole.logをStremとしてクライアント側に流せるようになるらしい
https://github.com/facebook/react/blob/ed3c65caf042f75fe2fdc2a5e568a9624c6175fb/packages/react-server/src/ReactFlightServer.js#L178-L198

mugimugi
mugimugi
mugimugi

クライアント側での処理の起点

mugimugi

resolveModel 周り