RSCとは?
※ RSCはまだReactチームにより開発中であり、本番環境にはまだ推奨されていないことに注意してください。
つまり、実験段階にあるためこの機能の実装の詳細は今後変更される可能性があります。
React Labs: 私たちが取り組んでいること – 2022 年 6 月
ReactServerComponents【RSC】はReactアプリのパフォーマンスを向上させる目的でサーバー側でレンダリングされるReactコンポーネントを作成する方法です。
React Server Componentsはサーバー側レンダリング (SSR)ではありません。
代替えでもございません。
どちらも名前に『サーバー』
が含まれており、どちらもサーバー上で作業を行っているため、紛らわしいかもしれません。
これは2つの別個の機能として理解する方がはるかに簡単です。
RSCを使用した場合、SSRを使用する必要性はございません。
ただし、SSRとRSCの両方を組み合わせて、サーバーコンポーネントでサーバー側のレンダリングを実行し、それらをブラウザで適切にhydrate(ハイドレート)することができます。
SSRとRSCの違いについては後ほど解説致します。
RSCはアプリ内の読み込み状態を管理できるReact機能のReact Suspenseとうまく連携してくれます。
ReactのSuspenseについて分からない場合はこちらのReact 18 Suspense 遅延読み込みとパフォーマンスを理解する 基礎の記事でSuspenseの概要を解説しておりますので参照ください。
ReactのRSCを使用すると、コンポーネントを定期的に再取得できます。
新しいデータがあるときに再レンダリングするコンポーネントを含むアプリケーションをサーバーで実行できるため、クライアントに送信する必要があるコードの量を制限できます。
サーバーコンポーネントを使用するためにすべてを理解する必要はありませんが、それがどのように機能するかについてある程度の理解は必要となります。
利点
なぜRSCが必要なのか?
RSCを使用する以前では、すべてのReactコンポーネントはクライアント側コンポーネントであり、すべてブラウザで実行されてきました。
これは、状態を追跡しイベントに応じてReactツリーを変更し、DOMを効率的に更新できます。
では、なぜわざわざサーバー上で何かをレンダリングしたいのでしょうか?
それらの利点を見ていきましょう。
RSCは、クライアントのコストを心配することなくバンドルサイズを縮小することで、開発者を助けてくれます。
任意のサイズの任意のプラグインを使用でき、コンポーネントはサーバー側で処理されるため、クライアントのブラウザはそれらをダウンロードする必要がまったくありません。
useEffectフックを使用したAPI呼び出しは、通常は何も問題はありません。
ですが、データフェッチアプローチでは、レンダリングするのに常に時間がかかりますし、それだけではございません。
その問題はバンドルサイズです。
バンドルサイズが大きいと、ダウンロードに時間がかかるためです。
RSCはこの問題を解決するのに役立ちます。
JavaScriptバンドルのサイズが小さくなり、ユーザーエクスペリエンスが向上します。
サーバーコンポーネントはバンドルに含まれません。
それらはブラウザによってダウンロードされることがないので、バンドルサイズにはまったく影響しません。
サーバーはデータベース、GraphQLエンドポイントおよびファイルシステムなどデータソースに直接アクセス可能です。
つまり、RSCはサーバー上で実行されるためネットワーク呼び出しを待つ必要がありません。
したがって、レンダリングプロセスが劇的に高速化されます。
ブラウザよりも高速にデータを取得できます。
サーバーでのレンダリングにはブラウザよりも優れた利点がございます。
サーバーとクライアントのコンポーネント分割と仕組み
まず、最初に気付くのはpackage.jsonファイルに実験的なバージョンを持つパッケージがいくつかあることです。
"react": "0.0.0-experimental-3310209d0", "react-dom": "0.0.0-experimental-3310209d0", "react-fetch": "0.0.0-experimental-3310209d0", "react-fs": "0.0.0-experimental-3310209d0", "react-pg": "0.0.0-experimental-3310209d0", "react-server-dom-webpack": "0.0.0-experimental-3310209d0",
react , react-dom, react-server-dom-webpackは RSCを実現する実験的なバージョンで、react-fetch , react-fs, react-pgは入出力システムとのやりとりに使うラッパーのパッケージ群となっています。
これらのパッケージ群はReact IO Libraries
と呼ばれます。
一部のコンポーネントはサーバー側でレンダリングされ、一部はクライアントでレンダリングされます。
では、サーバーコンポーネントとは何でしょうか?
RSCの操作プロセスを紹介する前に、いくつかの新しい概念を理解する必要がございます。
いくつかの基本原則のコンポーネントの種類があります。
Reactチームは、コンポーネントが書かれたファイルの拡張子に基づいてそれらを定義しています。
ファイルの末尾が.server.jsx
であればサーバーコンポーネントが含まれ、.client.jsx
であればクライアントコンポーネントが含まれます。
つまり、このRSCバージョンのReactを使用する場合、3つの異なるファイル拡張子を使用できるという事になります。
<SharedContainer> <ClientActionComponent onClick={open()}> <ServerTitleComponent /> </ClientActionComponent> <ServerListComponent> <ClientActionComponent onClick={addItem()}> </ServerListComponent> </SharedContainer>
・ MyComponent.client.tsx
これはクライアントコンポーネントです。
クライアント側コンポーネントは、クライアント側でのみレンダリングできるコンポーネントです 。
クライアントコンポーネントはサーバーコンポーネントを使用できません。
これらは通常、サーバーコンポーネントによってインポートされ、アプリケーションのインタラクティブな部分を表示するために使用されます。
サーバー側のデータソースにはアクセスできず、ステートフルであり、ブラウザAPIにアクセス可能です、.client.js、.client.jsx の形式で名前を付ける必要があります。
・ MyComponent.server.tsx
これはサーバーコンポーネントです。
サーバーコンポーネントは、サーバー側で実行されるコンポーネントです。
サーバー上のデータベースやファイルシステムなどのサーバー側のデータソースに直接アクセスできるため、データ取得プロセスがより高速かつ効率的になります。
サーバーコンポーネントはステートレスであり、サーバーコンポーネントはクライアントコンポーネントをインポート可能ですが、クライアントコンポーネントはサービスサーバーコンポーネントをインポートできず、.server.js、.server.jsxの形式で名前を付ける必要があります。
・ MyComponent.tsx
これは共有コンポーネントであり、両側で使用可能です。
共有コンポーネントは、それらを使用するコンポーネントの種類に応じて、サーバーまたはクライアントでレンダリングできるコンポーネントです。
一般的にはサーバーコンポーネントとクライアントコンポーネントが共有するいくつかの機能から構成されるコンポーネントであり、同様に共有コンポーネントも状態を持つことができません。
通常の.js
拡張子は、共有コンポーネント用です。
拡張子は.js
または.ts
どちらも使用していけます。
npm startコマンドでアプリケーションを起動すると、2つのタスクが同時に実行されます。
server/api.server.js
スクリプトを使用したNodeサーバーの実行です。
scripts/build.js
スクリプトによるクライアントサイド側のReactバンドル用Webpackのビルド
サーバスクリプトを見ると、app.server.js
がファイルにインポートされているのがわかります。
const ReactApp = require('../src/App.server').default;
これらのコンポーネントは、誰がインポートするかに応じて、サーバーまたはクライアントで実行できます。
その後、Node Writable streamとして処理されます。
const { pipeToNodeWritable } = require('react-server-dom-webpack/writer'); async function renderReactTree(res, props) { await waitForWebpack(); const manifest = readFileSync( path.resolve(__dirname, '../build/react-client-manifest.json'), 'utf8' ); const moduleMap = JSON.parse(manifest); pipeToNodeWritable(React.createElement(ReactApp, props), res, moduleMap); }
サーバー側でサービスを開始し、react-server-dom-webpack/writerのpipeToNodeWritableを使用してサーバーコンポーネントとデータとreact-client-manifest.jsonをチャンクデータストリームに変換し、クライアントに返します。
サーバー コンポーネントはHTMLとしてレンダリングされるのではなく、クライアントにストリーミングされる特別な形式としてレンダリングされます。
コンテンツのレンダリング方法にも違いがあるという事です。
現在のところ、ストリームには標準プロトコルはありませんが、JSON形式によく似ています。
応答の一部を下記に示します。
M1:{"id":"./src/SearchField.client.js","chunks":["client5"],"name":""}
つまり、サーバーは事前にレンダリングされたコンポーネントをHTMLではない内部形式で返します。
SSRでは初回のレンダリングで1 回しか使用されませんが、サーバーコンポーネントはデータを再レンダリングするために複数回再取得できます。
export default function NoteList({searchText}) { const notes = db.query( `select * from notes where title ilike $1 order by id desc`, ['%' + searchText + '%'] ).rows; return notes.length > 0 ? ( <ul className="notes-list"> {notes.map((note) => ( <li key={note.id}> <SidebarNote note={note} /> </li> ))} </ul> ) : ( <div className="notes-empty"> {searchText ? `Couldn't find any notes titled "${searchText}".` : 'No notes created yet!'}{' '} </div> ); }
分かりやすい例で仕組みを見て見ましょう。
サーバーコンポーネントはプライベートデータをフェッチすることもでき、先述したように.server.js
で終わるファイルで示されます。
アプリケーションを効率的にコーディングするには、サーバーとクライアント構造を使用してファイルを分割します。
データにアクセスする必要がある各コンポーネントは.server.js
になります
ユーザーと対話する必要がある各コンポーネントは.client.js
になります。
// ClientComponent.client.jsx export default function ClientComponent({ children }) { return ( <div> <h1> Hello from the client! </h1> {children} </div> ) } // ServerComponent.server.jsx export default function ServerComponent() { return <h2> Hello from the server! </h2> }
import ClientComponent from './ClientComponent.client' import ServerComponent from './ServerComponent.server' const OuterServerComponent = () => { return ( <ClientComponent> <ServerComponent /> </ClientComponent> ) }
上記のように、サーバーコンポーネントはReact クライアント側コンポーネントのクライアントコンポーネントツリーとサーバーコンポーネントツリーの識別と組み合わせて動作します。
これは、JavaScriptバンドルが読み込まれた後に発生します。
クライアントコンポーネントからサーバーコンポーネントをインポートしてレンダリングすることはできませんが、クライアントコンポーネントはReactNodeであるPropsを受け取ることができます。
React DocsでReact Server Componentsのプレゼン動画を視聴して、そのデモをクローンして試して見て下さい。
Introducing Zero-Bundle-Size React Server Components – React Blog
サーバーコンポーネントはサーバー上で実行され、クライアントコンポーネントはクライアント上で実行されるため、それぞれができることには多くの制限がございます。
覚えておくべき最も重要なことは、クライアントコンポーネントはサーバーコンポーネントをインポートできないということです。
これは、ブラウザで動作しないコードが含まれている可能性があるためです。
クライアントコンポーネントがサーバーコンポーネントに依存している場合、それらの依存関係をブラウザバンドルに取り込むことになります。
適用ルール
・ サーバーコンポーネントは状態を持つことができません。
これは、サーバーコンポーネントはサーバー上で要求ごとに1 回実行されるため、状態を持たずクライアントにのみ存在する機能を使用できません。
useStateおよびuseReducerフックはサポートされてませんので使用することができません。
そして、同様にRSCはuseEffect およびuseLayoutEffect などのレンダリングライフサイクルメソッドのReact フックも使用できません。
また、イベントも扱えませんのでonClickやonChangeなどは忘れてください。
・ ブラウザ専用APIが使えない
基本的には、サーバー上でポリフィルしないかぎり、DOMなどのブラウザ専用APIは使用できません。
ブラウザのみのAPIに依存するユーティリティ関数(カスタムフックなど)も使用できません。
APIを使用するには、クライアントコンポーネントを使用する必要があります。
従来のReactコンポーネントは、クライアント側でレンダリングされるため、クライアントコンポーネントと呼ばれます。
サーバーコンポーネントの強みは、ブラウザに送信される前にサーバーサイドでレンダリングされるため、例えばデータを取得するためのサーバー機能を利用できることです。
SSRとの違い
サーバー側のレンダリングは、最初のページの読み込みを減らすことに重点を置いています。
SSRはサーバー上でページ全体をレンダリングし、HTMLデータをユーザーに渡す技術です。
つまり、SSRを使用する場合は、HTMLをクライアントに送信します。
RSCはまったく別物です。
RSCは、クライアントの状態を失うことなくいつでも再レンダリングできます。
SSRアプリでも再レンダリングできますが、まったく新しいHTMLページを再レンダリングし、そのアプリの状態は失います。
RSCはツリーのどこからでも関数、データベースなどのサーバーデータソースにアクセス可能ですが、SSRでのアプリ、特にNext.jsではページのトップレベルでのみ機能する組み込み関数であるgetServerProps()を使用する必要があります。
最も重要な違いは、コンポーネントがまだクライアント側のコンポーネントであることです。
必要な依存関係はすべてダウンロードされます。
SSRを使用する場合での唯一の違いは、パフォーマンスを向上させるために最初のページロードにHTML のみが含まれることです。
最後に
RSCは本番環境で使用する準備がまだ完全にできていません。
実験段階であるので、実際にどのように役立つかを判断するのは困難となっています。
この機能は現在Node.jsサーバー環境でのみ使用できます。
ですが、RSCは今後Reactの将来で大きな部分として占める可能性がございます。
現在では、まだ準備ができていないかもしれませんが、近いうちに注意を払う時が来るのは間違いありません。
今のうちに、RSCの学習をしておくのは良い事でもあります。
ですがRSCアプリケーション用のコンポーネントを作成するには、慣れるまでに時間がかかる場合があります。
RSCは役立つとは思います、しかしこの機能がWebにおける革命をもたらしたのか?と言われるとそんな事はありません。
サーバーに要求する作業が増えれば増えるほど、クライアントに転送する必要があるデータが増える事になります。
React Server ComponentsとSSRはどちらもサーバー上でのReactレンダリングを伴いますが、実際にはまったく異なります。
RSCは非常に興味深い開発であり、私はRSCについてさらに学ぶことを今後楽しみにしています。
この記事が、RSCをより明確に理解するのに役立つことを願っています。
このテクノロジーは新しく、Next.jsおよびRemixなどのフレームワークと統合することもできます。
本日は以上となります。
最後までこの記事を読んで頂きありがとうございます。