React Server Component の Isomorphism について解説する
Next.js + React Server Component のリファレンス実装が出たので、手元で動かしながら理解したメモ。
これを書いてるモチベーションとして、Twitter を見る限り React Server Component のことを 「ただのサーバーサイドへの先祖返り」とか「SSR 結果を dangerouslySetInnerHtml してるだけでは?」みたいな反応があったので、そのへんの誤解を解きたい。
Introducing Zero-Bundle-Size React Server Components – React Blog
tl;dr
サーバーでのレンダリング結果を JSON シリアライズして、クライアントから要求できる。サーバー側での React Element の組み立ての過程にサーバー(Node.js)の API が使えるので、都度 API を実装する必要なく、RPC の中で隠蔽できる。一種の Isomorphism (クライアント・サーバー同型写像)。
これはつまり、サーバーで実行、更新される非同期コンポーネントであり、比較対象は Rails Hotwire, Phoenix LiveView の非同期 View などになると思われる。
React Server Component の目的
そもそも手段は一つだが、目的が二つ混ざっているので、これを整理する。
- ビルドサイズを重くなってしまうライブラリをサーバーサイドで実行して結果だけ返す。例えば marked や prettier、babel など
- RPC によって Client と Server で API の呼び出しを自然に隠蔽する Isomorpish を実現する
従来の React Element は関数参照などを扱うため、 JSON シリアライズができない構造体だったが、React Server Component は React Element のシリアライズしたものを、クライアントから要求できる。その代わり、JSON シリアライズできない関数などは渡せない。イベントハンドラを扱いたい場合は、 Server Component から Client Component を含んでレンダリングして、クライアント側でそれを Hydration する。
next.js 拡張のフルスタックフレームの blitz などでは、似たような実装がある。
blitz-js がどうやってサーバー上の関数のクライアントでの呼び出しを実現しているのか、調査した
ただの SSR 結果の HTML を受け取るのとの違い
*.server.js
はサーバーサイドでレンダリングされるが、その際に *.client.js
のクライアント側のコンポーネントも含めることができる。クライアントは純粋な HTML ではなく、React Element として受け取るので、受け取った際に、 Hydration が行われて、JS のロジック注入で、React Component としてクライアントで動的に振る舞う。
Server Component である components/App.server.js
をみると、その様子がわかる。
App.server.js
が SearchField.client
をマウントしている。
import React, { Suspense } from 'react'
import SearchField from './SearchField.client'
import Note from './Note.server'
import NoteList from './NoteList.server'
import AuthButton from './AuthButton.server'
import NoteSkeleton from './NoteSkeleton'
import NoteListSkeleton from './NoteListSkeleton'
export default function App({ selectedId, isEditing, searchText, login }) {
return (
<div className="container">
<!-- 中略 -->
<section className="sidebar-menu" role="menubar">
<SearchField />
<AuthButton login={login} noteId={null}>
Add
</AuthButton>
</section>
<nav>
<Suspense fallback={<NoteListSkeleton />}>
<NoteList searchText={searchText} />
</Suspense>
</nav>
</section>
<section className="col note-viewer">
<Suspense fallback={<NoteSkeleton isEditing={isEditing} />}>
<Note login={login} selectedId={selectedId} isEditing={isEditing} />
</Suspense>
</section>
</div>
</div>
)
}
入力として JSON を送って、関数ハンドラなどを扱わない React Element を返している。
実現される isomorphism
例えば Rails でも app/views/index.erb
の取得の際に、 ActiveRecord を叩いて SQL の実行結果を View に含んでいる。 それと同様に React Server Component の Element 組み立ての過程は Node.js で行われるので、サーバーサイドの機能をフルに使える。
ちょっと前に Next.js の getServerSideProps
で Prisma ORM を叩くサンプルを書いたが、 Server Component では hooks でラップした上で似たような書き味になると思う。
import type { GetServerSideProps } from "next";
import prisma from "../lib/prisma";
type Props = {
count: number;
};
export const getServerSideProps: GetServerSideProps<Props> = async (ctx) => {
const count = await prisma.user.count();
return {
props: {
count,
},
};
};
export default function Index(props: Props) {
return <div>user count: {props.count}</div>;
}
最初のデモでは /react
のエンドポイント、 next-server-component
では /api
で実装されていたが、RPC を一つ実装するだけで、参照系の API を実装する必要はなくなる。参照系のリソース参照は Server Component の組み立て過程に組み込まれているので、API としてクライアントに露出する必要がない。
とはいえ、更新系は別途実装する必要があり、更新が発生すると、 Server Component を再度取得し直して、その結果を React として差分更新する、といった実装を行う必要がある。
next-server-components/NoteEditor.client.js at master · vercel/next-server-components を読むと、Markdown を更新した際に、キャッシュを捨てて再取得することでプレビューし直すのが読み取れる。
これはつまり、サーバー上の state を使ってクライアントを更新する非同期 View であり、最初に述べたように、この比較対象は、 Rails Hotwire やそのネタ元であろう Phoenix LiveView になると思う。
Phoenix.LiveView — Phoenix LiveView v0.15.3
Hotwire と LiveView は JS を否定してこの実装になってるのに対して、 React Server Component は JS を積極的に活用して最適化やワークフロー改善を行った結果、このような実装になってる。Hotwire は全コネクションに対して Websocket で変更をプッシュするというワイルドな実装で、運用上の難を感じるので、React Server Component のほうが自分は伸びしろを感じる。やはりクライアント ・サーバーで同じ言語であるという利点が強い。
今後どのようになるか
現状のコードを読む限りは webpack のプラグインの使い方が難しかったり、専用のマニフェストの生成をサーバー実装から参照する必要があったりで、こなれていない。たぶんほとんどがボイラープレートになると思われるので、将来的に無設定で Node.js の http-server 上のエンドポイントとして動くコードが生成できるだろう。
これは完全に邪推だが、 Next.js でデモが出たのは、 React の SSR のサーバーの実装として一番使われているのが Next.js だったので、PHP が主流な Facebook 社内で開発していくより Next.js 開発元の Vercel と連携して開発していく、というメッセージだと思っている。その上で Facebook 社内でもあわよくば Server Component に切り替えたいのではないか? という気がする。experimental リポジトリでは react-pg
といった postgres の pg
のラッパーを用意しつつ、デモでは外部サーバーへの isomorphic fetch で外部リソースを参照しているように見せかけているのは、社内では PHP エンドポイントを叩きますよ、といったメッセージだと思っている。
しかし現状、Next.js 上で実装されているが API Route を使っているだけで、とくに Next.js としては組み込まれていない。 Next.js では無設定で使えて、単に getServerSideProps
が Server Component に切り替わる、というのが、今後一番ありそうなシナリオだと思っている。
Next.js 以外では、webpack のビルド結果を Node.js コンテナでマウントしてデプロイ、といった風になるのではないだろうか。
Discussion