React Server Components(以下RSC)学習としてReact公式ドキュメントを読んでいたところ、以下のようなコード例がありました。
// Server Component
import db from './database';
async function Page({id}) {
// Will suspend the Server Component.
const note = await db.notes.get(id);
// NOTE: not awaited, will start here and await on the client.
const commentsPromise = db.comments.get(note.id);
return (
<div>
{note}
<Suspense fallback={<p>Loading Comments...</p>}>
<Comments commentsPromise={commentsPromise} />
</Suspense>
</div>
);
}
// Client Component
"use client";
import {use} from 'react';
function Comments({commentsPromise}) {
// NOTE: this will resume the promise from the server.
// It will suspend until the data is available.
const comments = use(commentsPromise);
return comments.map(commment => <p>{comment}</p>);
}
ぱっと見で何をしているのかよくわからなかったし、理解するまで時間がかかったので、備忘録として残そうと思います。
RSCとは
ざっくりいうと、サーバー上で実行できるコンポーネントのことです。
RSCのメリット・デメリットなどの詳しい解説について、以下の記事が大変わかりやすかったです。
先に示した例のうち、PageコンポーネントはサーバーコンポーネントでCommentsコンポーネントはクライアントコンポーネントです。
なので、Pageはサーバーで実行され、CommentsはこれまでのReactと同様に、クライアントで実行されます。
Pageコンポーネント
改めてPageコンポーネントを見てみましょう。コメントなど一部改変しました。
// Server Component
import db from './database';
import { Suspense } from 'react';
import Comments from './Comments';
async function Page({id}) {
// awaitしているので、noteが取得できるまで先に進まない
const note = await db.notes.get(id);
// awaitしていないので、Promiseオブジェクトが入る
const commentsPromise = db.comments.get(note.id);
return (
<div>
{note}
<Suspense fallback={<p>Loading Comments...</p>}>
<Comments commentsPromise={commentsPromise} />
</Suspense>
</div>
);
}
ざっくりした処理の流れは以下のようになっています。
1. notesデータの取得
データベースからnoteのデータを取得しています。Pageコンポーネントはasync関数なので、awaitを使い、処理が先に進まないようにすることができます。
noteは比較的軽量なデータであると思っておいてください。
2. commentsデータの取得(Promiseの作成)
データベースからcommentのデータを取得しています。noteとは違い、あえてawaitを使っていません。そのため、commentsPromiseにはPromiseオブジェクトが入ります。
commentは非常にデータ量が多いと思っておいてください。
3. jsxの実行
return内に書かれているものが実行されます。noteについてはawaitをつけていたため、jsxが読み込まれる時には必ず値が取得できています。そのため、noteの内容が問題なく表示されます。
一方、Commentsコンポーネントに渡されているcommentsPromiseは、このコンポーネントがレンダリングされているときは、まだcommentデータを持っていない可能性があります、そのため、Suspenseを使って、commentsPromiseのPromiseが解決される(データの取得が完了する)までは、fallbackに渡されているものが、Commentsコンポーネントの代わりに表示されます。
Commentsコンポーネント
CommentsはPageとは違い、クライアントコンポーネントです。このコンポーネントはクライアントで実行されます。コメントの改変と、誤字の修正(mapのコールバック関数の引数がcommmentになっていた)を行いました。
// Client Component
"use client";
import {use} from 'react';
function Comments({commentsPromise}) {
// データが利用可能になるまで、処理は先に進みません
const comments = use(commentsPromise);
return comments.map(comment => <p>{comment}</p>);
}
ざっくりした処理の流れは以下のようになっています。
1. Promiseが解決されるのを待つ
ここでuseを使い、Promiseが解決されるまで処理が先に進まないようにしています。useはPromiseなどのリソースから値を読み取るためのReact APIです(公式ドキュメント)。useの引数にPromiseが渡された場合、Suspenseと協力して、これが解決するまではfallbackの内容を、解決したらchildrenのコンポーネントを表示します。
ちなみに、awaitではなくuseを使っている理由は、awaitは非同期関数内でしか利用できず、クライアントコンポーネントを非同期関数で実装することはできないからです。
2. jsxの実行
全てのコメントについて、1つ1つがpタグで囲まれた状態で出力されます。
commentsにデータが入るまで、この処理に進むことはないです。
なぜcomment取得時にawaitをつけないのか
noteが画面に表示されるまで時間がかかってしまうからです。
今回、noteは軽量なデータ、commentは非常に大きなデータだと仮定しました。commentの取得にawaitをつけてしまう(const commentsPromise = await db.comments.get(note.id);
とする)と、commentsが取得できるまで、処理が先に進めなくなってしまいます。noteはすでに取得が終わり、画面に表示できるにも関わらず、です。画面に何も表示されない時間が長くなってしまうことは、なるべく避けたいですよね。
そこで、comment取得にはあえてawaitを外しました。これにより、早期に取得が完了したnoteは画面に表示し、取得が終わっていないcommentについてはSuspenseを使って、現在取得中であることを示すようにしています。
RSCを使ったメリット
RSCコンポーネントには、初回読み込み時間の短縮、セキュアなデータ取得などのメリットがあります。今回の例のようにデータベースとの通信を行うコンポーネントは、まさにRSCの恩恵を受けることができます。
データベースから情報を取得する際、これまでのSPAでは、一度クライアントにJavaScriptが返され、これを実行することでデータフェッチが行われていました。つまり、HTMLやJavaScriptを要求するリクエストとデータフェッチリクエストが別々に行われており、クライアントとサーバーで複数回のやり取りが行われていました。
RSCを利用することにより、このクライアントとサーバーのやり取りの回数を減らすことができます。今回の例では、データベースとの通信ロジックは全てサーバーコンポーネントにまとめることで、データフェッチはサーバーとデータベース間の通信のやり取りすることができています。また、これによりクライアントはサーバーとの通信をするために仕方なく持っているセキュアな情報が不要になります。これはセキュリティという観点からも嬉しいことですね。
参考