先日、React がReact Server Componentsを予告しました。まだ開発中とのことなので使えるのは先になりそうですが、非常に面白い内容となっているのでこちらで共有したいと思います。
What is React Server Component
React Server Component とは、サーバーサイドのみで実行され、バンドルサイズへの影響を与えません。React Server Component はクライアントでダウンロードされないため、アプリの起動時間などの向上などが期待できます。
- 従来の React Component(以下 Client Comonent とする)
- React Server Component
また、Server Component は、データベースなどサーバサイドのデータソースにアクセスすることができたり、レンダリングする Client Component を動的に選択することができます。 ここで簡単な例に触れてみましょう。
React Server Component を使った例
以下はノートのタイトルと本文をサーバーサイドから取得し表示し、ノートのエディタを Client React コンポーネントとしてレンダリングする例です。
まず、Server Component を実装するには拡張子を.server.js
または.server.jsx
、.server.tsx
などにします。
import db from "db.server";
// (A1)
import NoteEditor from "NoteEditor.client";
function Note(props) {
const { id, isEditing } = props;
// (2) Can directly access server data sources during render, e.g. databases
const note = db.posts.get(id);
return (
<div>
<h1>{note.title}</h1>
<section>{note.body}</section>
{/* (A2) Dynamically render the editor only if necessary */}
{isEditing ? <NoteEditor note={note} /> : null}
</div>
);
}
この例からいくつか重要なことがわかります。
- A1 :Client React コンポーネントをインポートする時は
.client.js
または.client.jsx
,.client.tsx
の拡張子をつける。 - B : データベースなどのサーバーサイドのデータソースに直接アクセスしている
- A2: Client Component は
isEditing
がtrue
の時のみクライアントにロードされる。つまり必要に応じて動的にロードされることになる
続いて動的にロードされるNoteEditor
コンポーネント(Client Component)について見ていきましょう。
export default function NoteEditor(props) {
const note = props.note;
const [title, setTitle] = useState(note.title);
const [body, setBody] = useState(note.body);
const updateTitle = event => {
setTitle(event.target.value);
};
const updateBody = event => {
setTitle(event.target.value);
};
const submit = () => {
// ...save note...
};
return (
<form action="..." method="..." onSubmit={submit}>
<input name="title" onChange={updateTitle} value={title} />
<textarea name="body" onChange={updateBody}>
{body}
</textarea>
</form>
);
}
この例で重要な点は、Server Component の結果をクライアントにレンダリングするときに、以前にレンダリングされた可能性のある Client Component の状態を保持することです。具体的には、React は、サーバーから渡された新しい Props を既存のクライアントコンポーネントにマージし、これらのコンポーネントの状態(および DOM)を維持して、focus、state や進行中のアニメーションなどを保持します。
React Server Component のメリット
他にもたくさんのメリットがあるので今確認できるものを紹介していきます。
Zero-Bundle-Size Components
冒頭に述べたように、React Server Component はサーバーサイドのみで実行されるので、バンドルサイズが 0 となります。そのため開発時にコードサイズによるパフォーマンス低下を回避することができます。
// NoteWithMarkDown.js
// 従来のReactコンポーネントなのでそのままのバンドルサイズとなる
import marked from 'marked'; // 35.9K (11.2K gzipped)
import sanitizeHtml from 'sanitize-html'; // 206K (63.3K gzipped)
function NoteWithMarkdown({text}) {
const html = sanitizeHtml(marked(text));
return (/* render */);
}
// NoteWithMarkdown.server.js
// ServerComponentなのでバンドルサイズが0になる
import marked from 'marked'; // zero bundle size
import sanitizeHtml from 'sanitize-html'; // zero bundle size
function NoteWithMarkdown({text}) {
// same as before
}
バックエンドへのフルアクセス
React でアプリを作成する際の課題として、データへのアクセス方法及び、データの保存場所が挙げられるそうです。Server Component はバックエンドに直接アクセスできるので、例えば新しいアプリを作成し始めた時などに、データの保存場所がわからない場合にはファイルシステムを用いることもできます。
// Server Componentなのでバックエンドにアクセスできる
import fs from "react-fs";
function Note({ id }) {
const note = JSON.parse(fs.readFile(`${id}.json`));
return <NoteWithMarkdown note={note} />;
}
自動コード分割
コード分割により、アプリケーションを小さなバンドルに分割することができ、クライアントに送信するコードを減らせます。これの一般的なアプローチは、ルート毎にバンドルを遅延ロードするか、実行時になんらかの基準により異なるモジュールを遅延ロードすることが挙げられます。
// Client component
import React from "react";
// これらの1つは、クライアントでレンダリングされるとロードを開始する
const OldPhotoRenderer = React.lazy(() => import("./OldPhotoRenderer.js"));
const NewPhotoRenderer = React.lazy(() => import("./NewPhotoRenderer.js"));
function Photo(props) {
// Switch on feature flags, logged in/out, type of content, etc:
if (FeatureFlags.useNewPhotoRenderer) {
return <NewPhotoRenderer {...props} />;
} else {
return <PhotoRenderer {...props} />;
}
}
コード分割はパフォーマンス向上に役立ちますが、import 時にReact.lazy
を用いて動的インポートする必要が出てきます。また、このアプローチはコンポーネントのロード開始タイミングを遅らせるため、コードのロードする量を減らすといった利点を弱めてしまいます。
そこで、Server Component を用いることでこれらの問題に対応することができます。コード分割を自動化するために、Server Component は全ての Client Component を潜在的なコード分割点として扱います。
加えて、Server Component は開発者により早く使用するコンポーネントを選ばせることができるので、クライアントはレンダリングプロセスの早期段階でコンポーネントをダウンロードできます。
// Server Componentなので自動コード分割される
import React from "react";
// これらの1つは、レンダリングされてクライアントにストリーミングされると、ロードを開始する
import OldPhotoRenderer from "./OldPhotoRenderer.client.js";
import NewPhotoRenderer from "./NewPhotoRenderer.client.js";
function Photo(props) {
// Switch on feature flags, logged in/out, type of content, etc:
if (FeatureFlags.useNewPhotoRenderer) {
return <NewPhotoRenderer {...props} />;
} else {
return <PhotoRenderer {...props} />;
}
}
No waterfall
パフォーマンスが低下する原因の 1 つとして、アプリケーションがデータをフェッチするために連続してリクエストを行うときに発生します。たとえば、以下の例のように最初にコンポーネントをレンダリングしてから、useEffect()
内でデータをフェッチすることで生じます。
// Note.js
function Note(props) {
const [note, setNote] = useState(null);
useEffect(() => {
// 子のwaterfallによりレンダリング後にロードされます
fetchNote(props.id).then(noteData => {
setNote(noteData);
});
}, [props.id]);
if (note == null) {
return "Loading";
} else {
return (/* render note here... */);
}
}
親コンポーネントと子コンポーネント両方でこのアプローチをとってしまうと、子コンポーネントは親コンポーネントがデータをロードし終わるまでデータをロードし始めることができません。ただし、このアプローチをとるとアプリケーションが必要なデータを正確にフェッチすることができ、レンダリングされていない UI の部分のデータをフェッチしないようにするといったメリットがあります。
Server Component を使用すると、サーバとクライアントの連続した往復処理をサーバに移すことでこのメリットをそのままに、問題点を解決することができます。またこの往復処理をサーバーに移動することで、リクエストのレイテンシーを減らし、パフォーマンスを向上させることができます。さらに、Server Component はコンポーネント内から必要最小限のデータを直接フェッチし続けることができます。
// Note.server.js - Server Component
function Note(props) {
// サーバに低いレイテンシでデータにアクセスし、レンダリング中にロードされます
const note = db.notes.get(props.id);
if (note == null) {
// handle missing note
}
return (/* render note here... */);
}
まとめ
今回説明した Server Component の特徴を以下にまとめます。
- React Server Component はサーバーサイドのみで実行されるので、バンドルサイズが 0 となる
- Client Component(従来の React コンポーネント)と Server Component をわけるために
.client.js
、.server.js
のように拡張子を変える - データベース、ファイルシステムサーバー側のデータソースにアクセスできる
- レンダリングする Client Component を動的にロードできるのでクライアントではページのレンダリングに必要な最小限のコードのみダウンロードできる
- リロード時にクライアントの情報を保持する
さらに詳しい情報が知りたい場合は、公式からデモ動画を見て、デモ用のプロジェクトを試してみてください 🙌
参考
https://reactjs.org/blog/2020/12/21/data-fetching-with-react-server-components.html