React Server Componentを自分の言葉で説明できるようにする
【前提】
- SSR(Server Side Rendering)
- RSC(React Server COmponents)
の違いが曖昧。どちらもサーバーでJSX => HTMLにするんじゃないの?くらいの知識
読んだ
- JSX(e.g. <HogeConponent />)
- JSXによって生成されるツリー構造を持つオブジェクト
{
$$typeof: Symbol.for("react.element"),
type: 'html',
props: {
children: [
{
$$typeof: Symbol.for("react.element"),
type: 'head',
props: {
...
- HTML
の3要素がキーワードとして挙げられていた
文章の結論を見る感じ
RSCサーバー
- JSX => JSXツリーへの変換
SSRサーバー
- JSXツリー => HTMLへの変換
のように解釈できそうだったが、まだしっくりきていないので引き続き記事を読む
読む
Client Side Rendering
- 空のHTMLをサーバーから返し、クライアント(ブラウザ上でReactが動作し、HTMLを組み立てる)
Server Side Rendering
- サーバー上でReactが動作し、クライアントにHTMLを返し、そのあとでイベントハンドラを登録したりの処理がクライアント上で行われる(Hydration)
SSRで、
- サーバーでHTMLが構築される
- クライアントから情報取得のためにもう一度サーバーに通信を走らせる
つまり
クライアント(ページリクエスト)
↓
サーバー(HTML構築)
↓
クライアント(表示するデータリクエスト)
↓
サーバー(DBなどからデータ取得)
↓
クライアント(ページ完成)
この流れ、馬鹿馬鹿しくないか?と。
- データの取得
- HTMLの構築
この両方を1回のリクエストでやって仕舞えばいいのではないか。
でもReact
だけでは厳しかった(なんで無理だったのかは調査不足)
代わりに、Next.js
やGatsby
などのエコシステムがgetServerSideProps
などの解決策を生み出した
Next.js
で考える
getServerSideProps
=> React
によるレンダリング
のように、フレームワークによってReact実行の順序を制御することで無駄なリクエストを減らそうとしたということだと解釈
ただ、この方法にもいくつか問題点があり、Next.jsは『Reactをどう使うか』しか考慮できない。
つまり、コンポーネントの中でデータフェッチしたりすることは、Next.jsからしたら不可能。ゆえに、最上位のルートレベルで取得したデータをバケツリレーしたりする必要があった。
また、Reactは不要な場合にもクライアント上で常にハイドレーションを行う
そんな中Reactチームが出した解決方法がReact Server Components
import db from 'imaginary-db';
async function Homepage() {
const link = db.connect('localhost', 'root', 'passw0rd');
const data = await db.query(link, 'SELECT * FROM products');
return (
<>
<h1>Trending Products</h1>
{data.map((item) => (
<article key={item.id}>
<h2>{item.title}</h2>
<p>{item.description}</p>
</article>
))}
</>
);
}
export default Homepage;
こんなことが可能になる
サーバー・コンポーネント自体は驚くほど単純ですが、「React Server Components」パラダイムは非常に複雑です。その理由は、通常の古いコンポーネントがまだ残っていて、それらを組み合わせる方法が非常に入り組んだものである可能性があるためです。
この新しいパラダイムでは、私たちがよく知っている「従来の」Reactコンポーネントはクライアント・コンポーネント(Client Component)と呼ばれています。率直に言って、私はこの名称が好きではありません。😅
確かに、旧コンポーネントと新コンポーネントくらいの名称がいいのかな?
クライアントコンポーネントっていう呼び方だと、クライアントコンポーネントがサーバーでレンダリングされるっていう、言葉にしたら不思議なことが起こってしまう
ただ、新コンポーネント(サーバーコンポーネント)は絶対にサーバーでレンダリングされるのかな?
ただ、新コンポーネント(サーバーコンポーネント)は絶対にサーバーでレンダリングされるのかな?
この理解でよさそう
この新しいパラダイムでは、サーバー・コンポーネントという新しいタイプのコンポーネントが導入されました。この新しいコンポーネントはサーバー上でのみレンダリングされます。サーバー・コンポーネントのコードは、JSバンドルに含まれていないため、ハイドレーションや再レンダリングが行われることはありません。
ハイドレーションが行われないってどういうことだ?
export const Button = () => {
const onClick = () => { console.log('ほげ') };
return (
<button onClick={onClick} >hoge<?button>
)
}
これみたいなコンポーネントがクリックできるようになるのがハイドレーションなのかな?って思ってた
要するに
onsole.log('ほげ')
少なくともこのコードはJSバンドルに含まれていないとダメじゃない?ってこと
一旦今日はここまで、ここから先さらに深いことが書いてありそう
再開
Reactチームは次のルールを追加しました。クライアント・コンポーネントがインポートできるのは他のクライアント・コンポーネントのみである。
'use client'ディレクティブをArticleコンポーネントに追加すると、「クライアント・バウンダリ」が作成されます。このバウンダリ内のコンポーネントはすべて、暗黙的にクライアント・コンポーネントに変換されます。
親コンポーネントに'use client'
を指定して、クライアントコンポーネントであることを宣言したら、配下のコンポーネントも自動的にクライアントコンポーネントになるってことかな
アプリケーションの上の方でstateを使用しなければならない場合、どうすればよいのでしょうか?それは、すべてがクライアント・コンポーネントになる必要があるということでしょうか??
こう思っちゃうよなぁ。まだ理解できてない。今のところ、一番親で'use client'
書いたら全部クライアントでレンダリングされるって思ってる
多くの場合において、アプリケーションを再構築して、レンダリング元となるコンポーネントを工夫することで、この制約を回避できることが分かりました。
ほう。
'use client';
import { DARK_COLORS, LIGHT_COLORS } from '@/constants.js';
import Header from './Header';
import MainContent from './MainContent';
function Homepage() {
const [colorTheme, setColorTheme] = React.useState('light');
const colorVariables = colorTheme === 'light'
? LIGHT_COLORS
: DARK_COLORS;
return (
<body style={colorVariables}>
<Header />
<MainContent />
</body>
);
}
ダークモードとライトモードを切り替えるような場合を考える。モードをuseState
で状態管理している。
この場合
- useStateを使っている(ユーザー操作により、クライアントで再度レンダリングされる必要がある)
から、use client
必須
一番上のコンポーネントで'use client'
したぞ...どうなる...
'use client';
import { DARK_COLORS, LIGHT_COLORS } from '@/constants.js';
function ColorProvider({ children }) {
const [colorTheme, setColorTheme] = React.useState('light');
const colorVariables = colorTheme === 'light'
? LIGHT_COLORS
: DARK_COLORS;
return (
<body style={colorVariables}>
{children}
</body>
);
}
こんな感じで'use client'
を使っているコンポーネントを分割
import Header from './Header';
import MainContent from './MainContent';
import ColorProvider from './ColorProvider';
function Homepage() {
return (
<ColorProvider>
<Header />
<MainContent />
</ColorProvider>
);
}
こうやって使う
確かに'use client'
が書かれているファイルは分割できたが、親子関係はそのままだぜ
しかし、ちょっと待ってください。クライアント・コンポーネントであるColorProviderは、HeaderとMainContentの親です。いずれにせよ、まだツリーでは上位にありますね。
ただし、クライアント・バウンダリに関しては、親子関係は重要ではありません。Homepageは、HeaderとMainContentをインポートし、レンダリングするものです。これは、Homepageが、これらのコンポーネントのpropsが何であるかを決定することを意味します
ほう
覚えておいてください。私たちが解決しようとしている問題は、サーバー・コンポーネントは再レンダリングができないため、どのpropsにも新しい値を与えることができないということです。この新しい設定では、Homepageが、HeaderとMainContentのpropsを決定し、またHomepageはサーバー・コンポーネントであることから、問題はありません。
これは頭を悩ませる事柄です。何年もReactを経験した後の今でも、このことはきわめて分かりにくいと思っています😅。これに対する直感を養うためには、かなりの練習を必要としました。
ふむ...なんとなく言いたいことはわかるが
クライアントで再レンダリングされ得るものは、'use client'
じゃないと動作しない。
- わかる
- そりゃそう
クライアントで再レンダリングされないものは、サーバーでレンダリングされる(サーバーコンポーネント)
- ほう
親子関係は関係なく、単純にファイル単位(モジュール単位)で決まる
- こういうことか?
内部で何が起きているのか
function Homepage() {
return (
<p>
Hello world!
</p>
);
}
超シンプルなアプリケーション
ブラウザでアクセスすると以下のHTMドキュメントを受け取る
<!DOCTYPE html>
<html>
<body>
<p>Hello world!</p>
<script src="/static/js/bundle.js"></script>
<script>
self.__next['$Homepage-1'] = {
type: 'p',
props: null,
children: "Hello world!",
};
</script>
</body>
</html>
分かりやすくするため、ここであえて再構成を実施しました。たとえば、RSCコンテキストで生成された真のJSは、このHTMLドキュメントのファイル・サイズを削減するための最適化として、文字列化されたJSON配列を使用します。
また、HTMLの重要でない部分(<head>など)もすべて削除しました。
なるほど。この文字列化されたJSON配列ってのは、前の記事の『JSXによって生成されるツリー構造のオブジェクト』ってやつか?
HTMLドキュメントには、Reactアプリケーションによって生成されたUI、「Hello world!」パラグラフが含まれていることが分かります。これはサーバー・サイド・レンダリングのおかげであり、React Server Componentsに直接起因するものではありません。
あ"??
最後に、いくつかのインラインJSを含む2番目の<script>タグがあります。
self.__next['$Homepage-1'] = {
type: 'p',
props: null,
children: "Hello world!",
};
通常、Reactがクライアント上でハイドレーションを行うと、すべてのコンポーネントが素早くレンダリングされ、アプリケーションの仮想DOMが構築されます。サーバー・コンポーネントの場合、コードがJSバンドルに含まれていないため、これを行うことができません。
ここでいう通常っていうのは、クライアントサイドでReactがレンダリングを行う場合のことだよな。
サーバー・コンポーネントの場合はサーバーからのレスポンスがjsじゃなくてHTMLだからレンダリングできないと
したがって、レンダリングされた値と共に、サーバーによって生成された仮想DOMを送信します。Reactがクライアントにロードされると、そのディスクリプションを再生成する代わりに再利用します。
RSCはサーバーから
- レンダリングされた値(HTML)
- 仮想DOM
を送信し、、、
そのディスクリプションを再生成する代わりに再利用します。
これは何をしているの?
サーバーでレンダリングされたHTMLに対して、サーバーで生成された仮想DOMを用いてハイドレーションするってこと?
これ紹介されてた。読みテェが、かなりなゲェ
ので、一旦スキップ
サーバー・コンポーネントはサーバーを必要としない
- 静的:HTMLは、展開プロセス中に、アプリケーションのビルド時に生成されます。
- 動的:ユーザーがページをリクエストすると、HTMLは「オンデマンド」で生成されます。
React Server Componentsは、これらのレンダリング戦略のいずれとも互換性があります。
なんかすごいわかりそう
サーバーの環境でJSXで書かれたコンポーネントを
- HTML
- ハイドレーション用のjavascript
に変換するのがReact Server Componentなのか?
で、RSCのこの変換処理はいつ行われてもいい(リクエスト時でもビルド時でも)
ビルド時に行うとすると、JSXで書かれたコードを完全に静的な状態でリクエストを待機できる
読む
RSCは、このツリーを構成する一部のコンポーネントがサーバによってレンダリングされ、他のコンポーネントがブラウザによってレンダリングされることを可能にします。🤯
SSRとRSCって同じことしてない?って思っちゃうんよなぁ
最初のルートサーバコンポーネントをHTMLのタグとクライアントコンポーネントのプレースホルダで構成されるツリーとしてレンダリングすることです。 次に、そのツリーをシリアライズしてブラウザに送ります。 ブラウザ側では、受け取ったデータをデシリアライズし、プレースホルダを実際のクライアントコンポーネントに置き換え、最終結果をレンダリングします。
結局RCSは
- HTML
- サーバーでレンダリングできなかったコンポーネントをレンダリングするためのjs
をクライアントに送りつけるんだな
じゃあSSRはなんやねん
読む
クライアントレンダリングわかりやすすぎて安心するな
Page RouterのSSRのレスポンス
<!DOCTYPE html>
<html lang="en">
<head>
<meta charSet="utf-8"/>
<meta name="viewport" content="width=device-width"/>
<meta name="next-head-count" content="2"/>
<noscript data-n-css=""></noscript>
<script defer="" crossorigin="" nomodule="" src="/_next/static/chunks/polyfills-c67a75d1b6f99dc8.js"></script>
<script src="/_next/static/chunks/webpack-4e7214a60fad8e88.js" defer="" crossorigin=""></script>
<script src="/_next/static/chunks/framework-5429a50ba5373c56.js" defer="" crossorigin=""></script>
<script src="/_next/static/chunks/main-cb086c1786f15058.js" defer="" crossorigin=""></script>
<script src="/_next/static/chunks/pages/_app-b8840b4f8f2fad1f.js" defer="" crossorigin=""></script>
<script src="/_next/static/chunks/pages/index-3dedb261939be730.js" defer="" crossorigin=""></script>
<script src="/_next/static/mnu6FneGf2rg-_xRnbrTq/_buildManifest.js" defer="" crossorigin=""></script>
<script src="/_next/static/mnu6FneGf2rg-_xRnbrTq/_ssgManifest.js" defer="" crossorigin=""></script>
</head>
<body>
<div id="__next">
<h1>Hello,World</h1>
</div>
<script id="__NEXT_DATA__" type="application/json" crossorigin="">
{
"props": {
"pageProps": {
}
},
"page": "/",
"query": {
},
"buildId": "mnu6FneGf2rg-_xRnbrTq",
"nextExport": true,
"autoExport": true,
"isFallback": false,
"scriptLoader": [
]
}</script>
</body>
</html>
抜粋したjs
function(n, u, t) {
"use strict";
t.r(u),
t.d(u, {
default: function() {
return e
}
});
var r = t(5893);
function e() {
return (0,
r.jsx)("h1", {
children: "Hello,World"
})
}
}
Page RouterのSSRでは
- HTML
- コンポーネント情報を含むjs
の両方がレスポンスの中に入ってる
RSCは何やらRSC Payloadというものを出力するらしい
寄り道
まずRSC => RSC Payloadを生成する
RSC Payloadには以下の内容が含まれます。
- SCのレンダリング結果
- CCをレンダリングする場所のplaceholderとJavaScriptファイルへの参照
- SCからCCに渡されたprops
SCのレンダリング結果はHTMLのことかな
Next.jsはザックリ以下のような流れでSCとCCをレンダリングします。
サーバーサイド
- SCからRSC Payloadをレンダリングする
- RSC PayloadとCCから初期表示のためのHTMLをレンダリングする(SSR)
クライアントサイド
- 1-2で生成されたHTMLを元にすぐに非インタラクティブな画面を表示する
- RSC Payloadを元にSCとCCをツリー上で紐づけ、レンダリングする
- JavaScriptをhydrateし、CCをインタラクティブにする
個人的結論
RSCとは
単純にReactコンポーネントを2段階に分けて、HTMLにレンダリングするだけの仕組み
サーバーでの処理
- サーバーサイドでレンダリングできる静的なHTMLを出力(ブラウザでハイドレーションする必要がないコンポーネント)。この出力には、HTMLの他に、クライアントコンポーネントがどこにレンダリングされるかや、どのようにレンダリングされるかの情報も含む。この出力を2段階目の計算に渡す
クライアントでの処理
2. 残りのコンポーネントをHTMLにレンダリング & ハイドレーションでインタラクティブにする
以上
SSRとは
個人的にPage RouterとApp Routerでは少しSSRの処理内容が変わっているように思える
SSR (Page Router)
- クライアントからリクエストを受ける
- Reactを走らせて全部HTMLにレンダリング、ハイドレーション用のJSも生成
- クライアントに送信
- クライアントでハイドレーション
SSR (App Router)
- クライアントからリクエストを受ける
- React(RSC)を走らせてサーバーコンポーネントをHTMLにレンダリング、クライアントコンポーネントをレンダリングするための情報も併せて生成
- クライアントに送信
- クライアントでクライアントコンポーネントをHTMLにレンダリング & ハイドレーション
こういう理解であってるんじゃないか?