Open16

RSCの出力コードを読む

mizchimizchi

2年ぐらい RSC キャッチアップしてなかったので素朴に動かすところから。

$ npx create-next-app@latest rsc-research
$ cd rsc-research

CSS 周りを剥がしてほぼ最小状態にする

src/app/layout.tsx
import type { Metadata } from 'next'

export const metadata: Metadata = {
  title: 'Create Next App',
  description: 'Generated by create next app',
}

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  )
}
src/app/page.tsx
export default function Home() {
  return (
    <main>
      <h1>Home</h1>
    </main>
  )
}
mizchimizchi

Server Component を足してみる

src/app/Foo.tsx
export default function Foo() {
  return <div>Foo</div>
}

page.tsx からマウントする

src/app/page.tsx
import Foo from "./Foo";
export default function Home() {
  return (
    <main>
      <h1>Home</h1>
      <Foo />
    </main>
  )
}

next dev で動作確認。動いた。

追記: remix と混同していた。 .server の拡張子はいらない

mizchimizchi

ここから出力コードを読んでいく。一旦読みやすいように next を dev ビルドにする

next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  webpack: {
    dev: true
  }
}

module.exports = nextConfig

ビルド

$ npx next buid

.next 以下の出力から Foo のラベルがどこに吐かれてるか確認する。

$ ag -l Foo .next/
.next/trace
.next/server/app/index.html
.next/server/app/page.js
.next/server/app/index.rsc
mizchimizchi

それぞれの中身を確認すると、ここにあった。

.next/server/app/page.js
var jsx_runtime_ = __webpack_require__(6786);
;// CONCATENATED MODULE: ./src/app/Foo.server.tsx

function Foo() {
    return /*#__PURE__*/ jsx_runtime_.jsx("div", {
        children: "Foo"
    });
}

;// CONCATENATED MODULE: ./src/app/page.tsx


function Home() {
    return /*#__PURE__*/ (0,jsx_runtime_.jsxs)("main", {
        children: [
            /*#__PURE__*/ jsx_runtime_.jsx("h1", {
                children: "Home"
            }),
            /*#__PURE__*/ jsx_runtime_.jsx(Foo, {})
        ]
    });
}

ここにだけ Foo の実装が存在している

mizchimizchi

もうちょっとサーバーコンポーネントにしかできないことをやってみる

src/app/Foo.server.tsx
// should not be bundled in the client bundle
const keyboardCat = 'esoahisaidatshdioeasthdietshoa';

async function getAsyncValue( ) {
  await new Promise(r => setTimeout(r, 100));
  return keyboardCat.length;
}

export default async function Foo() {
  const value = await getAsyncValue();
  return <div>Foo: {value}</div>
}
  • 従来の React ではできなかった async な関数コンポーネント
  • クライアントからは keyboardCat の文字列の中身を知ることはできず、その文字列長だけがわかるとする

出力場所は特に変わらず

$ ag --ignore .next/trace -l Foo .next/
.next/trace
.next/server/app/index.html
.next/server/app/page.js
.next/server/app/index.rsc

中身を確認。

.next/server/app/page.js
// EXTERNAL MODULE: external "next/dist/compiled/react/jsx-runtime"
var jsx_runtime_ = __webpack_require__(6786);
;// CONCATENATED MODULE: ./src/app/Foo.server.tsx
// should not be bundled in the client bundle

const keyboardCat = "esoahisaidatshdioeasthdietshoa";
async function getAsyncValue() {
    await new Promise((r)=>setTimeout(r, 100));
    return keyboardCat.length;
}
async function Foo() {
    const value = await getAsyncValue();
    return /*#__PURE__*/ (0,jsx_runtime_.jsxs)("div", {
        children: [
            "Foo: ",
            value
        ]
    });
}

;// CONCATENATED MODULE: ./src/app/page.tsx


function Home() {
    return /*#__PURE__*/ (0,jsx_runtime_.jsxs)("main", {
        children: [
            /*#__PURE__*/ jsx_runtime_.jsx("h1", {
                children: "Home"
            }),
            /*#__PURE__*/ jsx_runtime_.jsx(Foo, {})
        ]
    });
}

特に非同期コンポーネントが加工されてるわけではなく、そのまま jsx_runtime_ に渡されている。これは誰が render している?

一応 keyboardCat の文字列が .next/server 以外には漏れていないことを確認

$ ag --ignore .next/trace -l esoahisaidatshdioeasthdietshoa .next/
.next/server/app/page.js

おk

mizchimizchi

.next/server の下で recat-dom で grep するとこの辺が引っかかる

.next/server/page.js
"use strict";
module.exports = require("next/dist/compiled/react-dom/server-rendering-stub");

react-dom/server-rendering-stub というものを呼んでいるっぽい。ここからは react-dom 側のソースコードを読んだほうがよさそう。

これっぽい。

https://github.com/facebook/react/blob/main/packages/react-dom/server-rendering-stub.js

https://github.com/facebook/react/blob/main/packages/react-dom/src/server/ReactDOMServerRenderingStub.js#L39C1-L43

// on the server we just call the callback because there is
// not update mechanism. Really this should not be called on the
// server but since the semantics are generally clear enough we
// can provide this trivial implementation.
function batchedUpdates<A, R>(fn: A => R, a: A): R {
  return fn(a);
}

export {batchedUpdates as unstable_batchedUpdates};

これは本当に stub で、1回しか実行しないから他のものに処理が委譲されている?

mizchimizchi

RSC のライブラリとしての実体はやはり react-server-dom-webpack らしい。直接の依存ではなく、next パッケージとして prubuid されている。

$ ag --ignore .next/trace -l react-server-dom-webpack .next/ node_modules/
.next/cache/next-server.js.nft.json
.next/server/app/page.js
.next/server/app/page.js.nft.json
.next/server/chunks/758.js
.next/static/chunks/698-1321e6d13d35448d.js
.next/next-server.js.nft.json
node_modules/next/dist/esm/server/app-render/use-flight-response.js
node_modules/next/dist/esm/server/app-render/entry-base.js
node_modules/next/dist/esm/server/app-render/create-server-components-renderer.js
node_modules/next/dist/esm/server/app-render/action-handler.js
node_modules/next/dist/esm/server/require-hook.js
node_modules/next/dist/esm/build/webpack/plugins/nextjs-require-cache-hot-reloader.js
node_modules/next/dist/esm/build/webpack/loaders/next-flight-loader/action-client-wrapper.js
node_modules/next/dist/esm/build/webpack/loaders/next-flight-loader/module-proxy.js
node_modules/next/dist/esm/build/webpack-config.js
node_modules/next/dist/esm/client/components/router-reducer/fetch-server-response.js
node_modules/next/dist/esm/client/components/router-reducer/reducers/server-action-reducer.js
node_modules/next/dist/esm/client/components/react-dev-overlay/internal/helpers/group-stack-frames-by-framework.js
node_modules/next/dist/esm/client/app-index.js
node_modules/next/dist/server/app-render/use-flight-response.js
node_modules/next/dist/server/app-render/entry-base.js
node_modules/next/dist/server/app-render/create-server-components-renderer.js
node_modules/next/dist/server/app-render/action-handler.js
node_modules/next/dist/server/require-hook.js
node_modules/next/dist/compiled/react-server-dom-webpack-experimental/node-register.js
node_modules/next/dist/compiled/react-server-dom-webpack-experimental/server.edge.js
node_modules/next/dist/compiled/react-server-dom-webpack-experimental/client.edge.js
node_modules/next/dist/compiled/react-server-dom-webpack-experimental/server.node.unbundled.js
node_modules/next/dist/compiled/react-server-dom-webpack-experimental/server.browser.js
node_modules/next/dist/compiled/react-server-dom-webpack-experimental/index.js
node_modules/next/dist/compiled/react-server-dom-webpack-experimental/client.node.unbundled.js
node_modules/next/dist/compiled/react-server-dom-webpack-experimental/client.browser.js
node_modules/next/dist/compiled/react-server-dom-webpack-experimental/package.json
node_modules/next/dist/compiled/react-server-dom-webpack-experimental/client.node.js
node_modules/next/dist/compiled/react-server-dom-webpack-experimental/server.node.js
node_modules/next/dist/compiled/react-server-dom-webpack-experimental/plugin.js
node_modules/next/dist/compiled/react-server-dom-webpack-experimental/cjs/react-server-dom-webpack-server.node.production.min.js
node_modules/next/dist/compiled/react-server-dom-webpack-experimental/cjs/react-server-dom-webpack-server.node.unbundled.production.min.js
node_modules/next/dist/compiled/react-server-dom-webpack-experimental/cjs/react-server-dom-webpack-client.browser.production.min.js
node_modules/next/dist/compiled/react-server-dom-webpack-experimental/cjs/react-server-dom-webpack-server.browser.development.js
node_modules/next/dist/compiled/react-server-dom-webpack-experimental/cjs/react-server-dom-webpack-node-register.js
node_modules/next/dist/compiled/react-server-dom-webpack-experimental/cjs/react-server-dom-webpack-server.edge.production.min.js
node_modules/next/dist/compiled/react-server-dom-webpack-experimental/cjs/react-server-dom-webpack-server.browser.production.min.js
node_modules/next/dist/compiled/react-server-dom-webpack-experimental/cjs/react-server-dom-webpack-client.node.unbundled.development.js
node_modules/next/dist/compiled/react-server-dom-webpack-experimental/cjs/react-server-dom-webpack-client.node.production.min.js
node_modules/next/dist/compiled/react-server-dom-webpack-experimental/cjs/react-server-dom-webpack-client.browser.development.js
node_modules/next/dist/compiled/react-server-dom-webpack-experimental/cjs/react-server-dom-webpack-server.edge.development.js
node_modules/next/dist/compiled/react-server-dom-webpack-experimental/cjs/react-server-dom-webpack-server.node.development.js
node_modules/next/dist/compiled/react-server-dom-webpack-experimental/cjs/react-server-dom-webpack-client.edge.production.min.js
node_modules/next/dist/compiled/react-server-dom-webpack-experimental/cjs/react-server-dom-webpack-client.node.unbundled.production.min.js
node_modules/next/dist/compiled/react-server-dom-webpack-experimental/cjs/react-server-dom-webpack-server.node.unbundled.development.js
node_modules/next/dist/compiled/react-server-dom-webpack-experimental/cjs/react-server-dom-webpack-plugin.js
node_modules/next/dist/compiled/react-server-dom-webpack-experimental/cjs/react-server-dom-webpack-client.node.development.js
node_modules/next/dist/compiled/react-server-dom-webpack-experimental/cjs/react-server-dom-webpack-client.edge.development.js
node_modules/next/dist/compiled/react-server-dom-webpack/node-register.js
node_modules/next/dist/compiled/react-server-dom-webpack/server.edge.js
node_modules/next/dist/compiled/react-server-dom-webpack/client.edge.js
node_modules/next/dist/compiled/react-server-dom-webpack/server.node.unbundled.js
node_modules/next/dist/compiled/react-server-dom-webpack/server.browser.js
node_modules/next/dist/compiled/react-server-dom-webpack/index.js
node_modules/next/dist/compiled/react-server-dom-webpack/client.node.unbundled.js
node_modules/next/dist/compiled/react-server-dom-webpack/client.browser.js
node_modules/next/dist/compiled/react-server-dom-webpack/package.json
node_modules/next/dist/compiled/react-server-dom-webpack/server.node.js
node_modules/next/dist/compiled/react-server-dom-webpack/plugin.js
node_modules/next/dist/compiled/react-server-dom-webpack/client.node.js
node_modules/next/dist/compiled/react-server-dom-webpack/cjs/react-server-dom-webpack-server.node.unbundled.production.min.js
node_modules/next/dist/compiled/react-server-dom-webpack/cjs/react-server-dom-webpack-server.node.production.min.js
node_modules/next/dist/compiled/react-server-dom-webpack/cjs/react-server-dom-webpack-server.browser.development.js
node_modules/next/dist/compiled/react-server-dom-webpack/cjs/react-server-dom-webpack-client.browser.production.min.js
node_modules/next/dist/compiled/react-server-dom-webpack/cjs/react-server-dom-webpack-server.edge.production.min.js
node_modules/next/dist/compiled/react-server-dom-webpack/cjs/react-server-dom-webpack-node-register.js
node_modules/next/dist/compiled/react-server-dom-webpack/cjs/react-server-dom-webpack-client.node.unbundled.development.js
node_modules/next/dist/compiled/react-server-dom-webpack/cjs/react-server-dom-webpack-server.browser.production.min.js
node_modules/next/dist/compiled/react-server-dom-webpack/cjs/react-server-dom-webpack-client.development.js
node_modules/next/dist/compiled/react-server-dom-webpack/cjs/react-server-dom-webpack-client.production.min.js
node_modules/next/dist/compiled/react-server-dom-webpack/cjs/react-server-dom-webpack-client.node.production.min.js
node_modules/next/dist/compiled/react-server-dom-webpack/cjs/react-server-dom-webpack-server.node.development.js
node_modules/next/dist/compiled/react-server-dom-webpack/cjs/react-server-dom-webpack-client.edge.production.min.js
node_modules/next/dist/compiled/react-server-dom-webpack/cjs/react-server-dom-webpack-server.edge.development.js
node_modules/next/dist/compiled/react-server-dom-webpack/cjs/react-server-dom-webpack-client.browser.development.js
node_modules/next/dist/compiled/react-server-dom-webpack/cjs/react-server-dom-webpack-server.node.unbundled.development.js
node_modules/next/dist/compiled/react-server-dom-webpack/cjs/react-server-dom-webpack-client.node.unbundled.production.min.js
node_modules/next/dist/compiled/react-server-dom-webpack/cjs/react-server-dom-webpack-plugin.js
node_modules/next/dist/compiled/react-server-dom-webpack/cjs/react-server-dom-webpack-client.node.development.js
node_modules/next/dist/compiled/react-server-dom-webpack/cjs/react-server-dom-webpack-client.edge.development.js
node_modules/next/dist/build/webpack/plugins/nextjs-require-cache-hot-reloader.js
node_modules/next/dist/build/webpack/loaders/next-flight-loader/module-proxy.js
node_modules/next/dist/build/webpack/loaders/next-flight-loader/action-client-wrapper.js
node_modules/next/dist/build/webpack-config.js
node_modules/next/dist/client/components/router-reducer/fetch-server-response.js
node_modules/next/dist/client/components/router-reducer/reducers/server-action-reducer.js
node_modules/next/dist/client/components/react-dev-overlay/internal/helpers/group-stack-frames-by-framework.js
node_modules/next/dist/client/app-index.js

見る限り -experimental 版もある。

mizchimizchi

ServerActions を使ってみる

https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions#convention

next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  experimental: {
    serverActions: true,
  },
}
src/app/actions.ts
'use server';

const keyboardCat = 'euoaueoaueoauoeathitnosaiseoadiu';

export async function asyncAction() {
  return keyboardCat.length;
}

これをクライアントから呼ぶ。

src/app/Client.ts
'use client';
import { useCallback } from 'react';
import { asyncAction } from './actions';
export default function ClientComponent() {
  const onClick = useCallback(() => {
    (async () => {
      const value = await asyncAction();
      console.log('value', value);
    })();
  }, []);
  return (
    <button
      onClick={onClick}
    >
      Call server function
    </button>
  )
}

page.tsx から呼ぶ

src/app/page.tsx
import ClientComponent from "./Client";
import Foo from "./Foo";
export default function Home() {
  return (
    <main>
      <h1>Home</h1>
      <Foo />
      <ClientComponent />
    </main>
  )
}

動いた

クライアントに漏れてないことを確認

$ ag --ignore .next/trace -l euoaueoaueoauoeathitnosaiseoadiu .next/ 
.next/server/app/page.js