POSTD PRODUCED BY NIJIBOX

POSTD PRODUCED BY NIJIBOX

ニジボックスが運営する
エンジニアに向けた
キュレーションメディア

Josh W. Comeau

本記事は、原著者の許諾のもとに翻訳・掲載しております。

私も年を取ったと感じるのは、今年Reactが10年目を迎えたからです。

混乱していた開発コミュニティにReactが初めて紹介されてから10年、以来いくつもの進化を遂げてきました。Reactチームは、急進的な改革ということに関しては躊躇しませんでした。問題に対して、より良い解決策が見つかれば、それを実行してきました。

数か月前、Reactチームは最新のパラダイム・シフトであるReact Server Componentsを発表しました。史上初めて、Reactコンポーネントがサーバーでのみ実行できるようになったのです。

このことに関連してオンライン上では、きわめて大きな混乱が起きています。それが何なのか、どのように機能するのか、利点は何か、そしてSSR(Server Side Rendering)などとどのように連携するのか、多くの人が多くの疑問を抱いています。

私はReact Server Componentsに関する数多くの実験を行ってきており、私自身が持っていた多くの疑問に対する回答を得てきました。正直に言うと、この技術に関して私は予想していたよりもはるかに興奮しています。本当に素晴らしい技術です!

そこで今日の私の目標は、このことを分かりやすく解説し、React Server Componentsについて皆さんが持っている多くの疑問に答えることです。

none

対象読者

このチュートリアルは主として、すでにReactを使用していて、React Server Componentsに関心がある開発者の方々を対象として書かれています。Reactの専門家である必要はありませんが、Reactを使い始めたばかりの場合はだいぶ混乱するかもしれません。

Server Side Renderingの簡単解説

React Server Componentsを理解するうえで、SSRがどのように機能するかを理解することが役立ちます。すでにSSRについて理解している場合は、次の見出しに進んでください。 私が2015年にReactを使い始めたとき、ほとんどのReactセットアップでClient Side Rendering(CSR)戦略が用いられていました。ユーザーが受け取るのは次のようなHTMLファイルでした。

<!DOCTYPE html>
<html>
  <body>
    <div id="root"></div>
    <script src="/static/js/bundle.js"></script>
  </body>
</html>

このbundle.jsスクリプトには、Reactや他のサードパーティの依存関係、自分たちが作成したすべてのコードなど、アプリケーションをマウントして実行するために必要なものがすべて含まれています。

JSがダウンロードされて解析されると、Reactが動作し始め、アプリケーション全体のすべてのDOMノードを呼び出し、それを空の<div id="root">に格納します。

このアプローチの問題点は、すべての作業を実行するのに時間がかかることです。しかもその作業中、ユーザーは何も表示されていない白い画面を見つめ続けることになります。この問題は時間の経過につれて悪化する傾向があります。新しい機能がリリースされるたびに、JavaScriptバンドルに新たに多くのキロバイトが追加され、ユーザーが座って待つ時間が長くなっていきます。1

サーバー・サイド・レンダリングは、こうした状況を改善するために設計されました。空のHTMLファイルを送信する代わりに、サーバーはアプリケーションをレンダリングして実際のHTMLを生成します。ユーザーは完全に形成されたHTMLドキュメントを受け取ることになります。

双方向性の処理には、引き続きクライアント上でReactが動作することが必要なため、そのHTMLファイルには従来通り<script>タグが含まれています。ただし、ブラウザ内で若干異なる動作をするようReactを構成します。最初からすべてのDOMノードを呼び出す代わりに、既存のHTMLを採用するのです。このプロセスはハイドレーションと呼ばれています。

このことに関しては、ReactのコアチームメンバーであるDan Abramovによる次の説明が気に入っています。

Hydration is like watering the “dry” HTML with the “water” of interactivity and event handlers.(ハイドレーションとは、「乾いた」HTMLに双方向性とイベント・ハンドラーの「水」を注ぐようなものである。)

JSバンドルがダウンロードされると、Reactはアプリケーション全体を素早く実行して、UIの仮想スケッチを構築し、それを実際のDOMに「フィット」させ、イベント・ハンドラーをアタッチし、エフェクトを発火させるなどの機能を実行します。

それが、つまりSSRです。サーバーが最初のHTMLを生成するため、JSバンドルがダウンロードされ解析されている間、ユーザーは真っ白なページを見つめている必要がなくなります。次に、サーバー・サイドのReactが中断したところからクライアントサイドのReactが引き継ぎ、DOMを採用し、双方向性を散りばめます。

none

包括的な用語

サーバー・サイド・レンダリングについて話しをするとき、私たちは通常、次のようなフローを想像します。

  1. ユーザーがmyWebsite.comにアクセスする。
  2. Node.jsサーバーがリクエストを受け取り、すぐにReactアプリケーションをレンダリングして、HTMLを生成する。
  3. その生成されたばかりのHTMLがクライアントに送信される。

これはサーバー・サイド・レンダリングを実装することを可能にする1つの方法ですが、これが唯一の方法ではありません。もう1つのオプションとして、アプリケーションの構築時にHTMLを生成する方法があります。

通常、Reactアプリケーションはコンパイルして、JSXをプレーンでなじみのあるJavaScriptに変換し、すべてのモジュールをバンドルする必要があります。同じプロセス中に、異なるルートのすべてでHTMLを全部「事前レンダリング」した場合はどうなるでしょうか?

これが一般に、「静的サイト生成(SSG)」として知られているものです。これはサーバー・サイド・レンダリングの派生型です。

私の考えでは、「サーバー・サイド・レンダリング」は、いくつかの異なるレンダリング戦略を含む包括的な用語です。これらのすべてには、1つの共通点があります。それは、最初のレンダリングがReactDOMServer APIを使用して、Node.jsなどのサーバーランタイムで行われることです。実際には、これがいつ発生するのか、オンデマンドなのかコンパイル時点なのかは問題になりません。いずれにせよ、それはサーバー・サイド・レンダリングなのです。

前後に跳ねる

Reactでのデータフェッチについてお話ししましょう。通常、ネットワーク経由で通信するアプリケーションには次の2種類があります。

  • クライアント側のReactアプリ
  • サーバー側のREST API

React QueryやSWR、Apolloなどを使用して、クライアントはバックエンドにネットワーク・リクエストを送信し、バックエンドはデータベースからデータを取得し、そのデータをネットワーク経由で送り返します。

そのフローは、グラフを使って可視化できます。

(訳注:イメージは原文サイトの表示のキャプチャー画像です)

none

グラフに関する注記

このブログ記事には、こうした「ネットワーク・リクエスト・グラフ」がいくつか含まれています。これらは、いくつかの異なるレンダリング戦略において、クライアント(ブラウザ)とサーバー(バックエンドAPI)の間でデータがどのように移動されるのかを可視化するためのものです。

下辺の数字は、仮想の時間単位を表しています。分でも秒でもありません。実際には、数値はさまざまな要因により、大きく変化します。このブログで取り上げているグラフは、概念の概要を理解することを目的としたもので、実際のデータをモデル化したものではありません。

この最初のグラフは、CSR戦略を使用したフローを表しています。始まりはクライアントによるHTMLファイルの受信です。このファイルにはコンテンツはありませんが、1つ以上の<script>タグがあります。

JSがダウンロードされ、解析されると、Reactアプリが起動し、数多くのDOMノードが作成され、UIが構築されます。ただし、最初は実際のデータがないため、読み込み状態でシェル(ヘッダー、フッター、一般的なレイアウト)をレンダリングすることしかできません。

このようなパターンは、これまでにたくさん見たことがあるのではないでしょうか。たとえば、UberEatsではシェルのレンダリングから始まり、実際のレストランを表示するために必要なデータをフェッチします。

ネットワーク・リクエストが解決され、Reactが再レンダリングを実行し、読み込み中のUIが実際のコンテンツに置き換えられるまで、この読み込み状態が表示され続けます。

別の構築方法を見てみましょう。次のグラフは、同じ一般的なデータ・フェッチ・パターンを維持していますが、CSRではなくサーバー・サイド・レンダリングを使用しています。

(訳注:イメージは原文サイトの表示のキャプチャー画像です)

この新しいフローでは、最初のレンダリングはサーバー上で実行されます。それは、ユーザーが受け取るのは完全には空でないHTMLファイルであることを意味します。

(白紙のページよりもシェルの方が優れているという意味で)これは進歩ですが、結局のところ、目立って大きな変化を引き起こすものではありません。ユーザーは読み込み画面を見るためにアプリにアクセスしているのではなく、コンテンツ(レストラン、ホテルのリスト、検索結果、メッセージなど)を見るためにアクセスしているのです。

ユーザー・エクスペリエンスの違いを真に理解するため、いくつかのWeb性能測定基準をグラフに追加してみることにしましょう。この2つのフローを交互に切り替えて、フラグに何が起こるかを注意して見ていてください。

(訳注:イメージは原文サイトの表示のキャプチャー画像です)

これらのフラグはそれぞれが、一般的に使用されているWeb性能測定基準を表します。以下がその説明です。

  • ファースト・ペイント—ユーザーは、何も映っていない真っ白な画面を見つめる必要がなくなります。全体的なレイアウトはレンダリングされましたが、コンテンツはまだ表示されません。FCP(First Contentful Paint)とも呼ばれています。
  • ページ・インタラクティブ— Reactがダウンロードされ、アプリケーションがレンダリングされ、ハイドレーションが行われました。インタラクティブな要素は完全に応答するようになっています。TTI(Time To Interactive)とも呼ばれています。
  • コンテンツ・ペイント—ページに、ユーザーが関心のあるコンテンツが含まれるようになっています。データベースからデータを取得し、UIにレンダリングしました。LCP(Largest Contentful Paint)とも呼ばれています。

サーバー上で最初のレンダリングを行うことで、最初の「シェル」をより素早く描画することが可能になります。そのため、前に進んでいる感覚が得られ、読み込みエクスペリエンスが多少速く感じられるようになります。

また状況によっては、これは有意義な改善になります。たとえばユーザーが、ナビゲーション・リンクをクリックできるよう、ヘッダーが読み込まれるのを待っているだけということがあるでしょう。

しかし、この流れは少々バカげていると思えませんか?SSRのグラフを見れば、リクエストがサーバー上で始まっていることは一目瞭然です。2つ目のラウンドトリップ・ネットワーク・リクエストを求める代わりに、最初のリクエスト中にデータベース作業を実行してみてはどうでしょうか。

つまり、次のようなことをしてみるのはどうでしょうということです。

(訳注:イメージは原文サイトの表示のキャプチャー画像です)

クライアントとサーバーの間を行ったり来たりする代わりに、最初のリクエストの一部としてデータベース・クエリーを実行し、完全に構築されたUIをユーザーに直接送信します。

しかし、うーん、厳密にはどうすればいいのでしょう?

これが機能するためには、データベース・クエリーを実行するために、サーバー上で排他的に実行される大量のコードをReactに提供できなければなりません。しかし、Reactではそれを選択することができませんでした。サーバー・サイド・レンダリングの場合でも、コンポーネントのすべてがサーバーとクライアントの両方でレンダリングされることになります。

エコシステムは、この問題に対する数多くの解決策を考え出しました。Next.jsやGatsbyなどのメタフレームワーク 2 では、サーバー上でのみコードを実行する独自の方法を編み出しました。

たとえば、(従来の「Pages」ルーターを使用する)Next.jsを使用する場合は次のようになります。

import db from 'imaginary-db';
// This code only runs on the server:
export async function getServerSideProps() {
  const link = db.connect('localhost', 'root', 'passw0rd');
  const data = await db.query(link, 'SELECT * FROM products');
  return {
    props: { data },
  };
}
// This code runs on the server + on the client
export default function Homepage({ data }) {
  return (
    <>
      <h1>Trending Products</h1>
      {data.map((item) => (
        <article key={item.id}>
          <h2>{item.title}</h2>
          <p>{item.description}</p>
        </article>
      ))}
    </>
  );
}

これを分析してみましょう。サーバーがリクエストを受け取ると、getServerSideProps関数が呼び出されます。それが、propsオブジェクトを返します。これらのpropsはコンポーネントに取り込まれ、そのコンポーネントが最初にサーバー上でレンダリングされ、次にクライアント上でハイドレーションされます。

ここで、優れている点は、getServerSidePropsがクライアント上で再実行されないことです。実際、この関数はJavaScriptバンドルにも含まれません。

このアプローチは時代のはるか先をいくものでした。正直に言って、それは驚くほどすごいことでした。ただし、これにはいくつかの問題点があります。

  1. この戦略は、ツリーの最上位にあるコンポーネントに関して、ルート・レベルでしか機能しません。どんなコンポーネントでも行うことができるものではないのです。
  2. 各メタフレームワークには独自のアプローチがあります。Next.jsには1つのアプローチがあり、Gatsbyには別のアプローチがあり、Remixにはさらに別のアプローチがあります。これまでのところ、標準化されていません。
  3. すべてのReactコンポーネントは、たとえ不要な場合でも、クライアント上で常にハイドレーションを行います。

何年もの間、Reactチームは正式な解決方法を考案するために、この問題に密かに取り組んできました。その解決策こそ、今React Server Componentsと呼ばれているものです。

React Server Componentsの紹介

高いレベルでは、React Server Componentsはまったく新しいパラダイムの名称です。その新しい世界では、サーバー上で排他的に実行されるコンポーネントを作成できます。そのため、Reactコンポーネント内でデータベース・クエリーを直接記述することが可能になります!

次に、「サーバー・コンポーネント」の簡単な例を示します。

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を使用してきた私には、最初このコードはきわめて突飛なものに思えました。😅

「ちょっと待て!」と本能が叫びました。「関数コンポーネントは非同期になることはできないぞ!そして、そのようなレンダリングで直接副作用を発生させることは許されない!」

キーとなるのは、次のことを理解することです。サーバー・コンポーネントが再レンダリングされることは決してありません。サーバー・コンポーネントはサーバー上で1回だけ実行され、UIを生成します。レンダリングされた値はクライアントに送信され、所定の場所にロックされます。Reactに関しては、この出力は変更不能であり、決して変わりません 3

つまり、ReactのAPIの大部分は、サーバー・コンポーネントと互換性がないことを意味します。たとえば、stateは変化する可能性がありますが、サーバー・コンポーネントは再レンダリングできないため、stateは使用できません。また、effectはクライアント上でレンダリング後にのみ実行され、サーバー・コンポーネントがクライアントに到達することはないため、effectを使用することはできません。

これはまた、ルールに関してもう少し柔軟性があることも意味しています。たとえば、従来のReactでは、レンダリングのたびに副作用が繰り返されることがないよう、useEffectコールバックやイベント・ハンドラーなどの中に副作用を配置する必要がありました。しかし、コンポーネントが1回しか実行されないのであれば、そのことについて心配する必要はなくなります。

サーバー・コンポーネント自体は驚くほど単純ですが、「React Server Components」パラダイムは非常に複雑です。その理由は、通常の古いコンポーネントがまだ残っていて、それらを組み合わせる方法が非常に入り組んだものである可能性があるためです。

この新しいパラダイムでは、私たちがよく知っている「従来の」Reactコンポーネントはクライアント・コンポーネント(Client Component)と呼ばれています。率直に言って、私はこの名称が好きではありません。😅

「クライアント・コンポーネント」という名称は、これらのコンポーネントがクライアント上でのみレンダリングされるかのような意味に取れますが、実際にはそうではありません。クライアント・コンポーネントはクライアントとサーバーの両方でレンダリングできます

(訳注:イメージは原文サイトの表示のキャプチャー画像です)

これらの用語は非常に分かりにくいので、次のように要約してみました。

  • React Server Componentsとは、この新しいパラダイムの名称です。
  • この新しいパラダイムでは、私たちがよく知っていて好んで使用してきた「標準」Reactコンポーネントが、クライアント・コンポーネントへと名称変更されました。古いものに新しい名前が付けられたのです。
  • この新しいパラダイムでは、サーバー・コンポーネントという新しいタイプのコンポーネントが導入されました。この新しいコンポーネントはサーバー上でのみレンダリングされます。サーバー・コンポーネントのコードは、JSバンドルに含まれていないため、ハイドレーションや再レンダリングが行われることはありません。
none

React Server Components vs.サーバー・サイド・レンダリング

もう1つ別の一般的な混乱を解消しましょう。React Server Componentsはサーバー・サイド・レンダリングに代わるものではありません。React Server Componentsを「SSRバージョン2.0」とは考えないでください。

代わりに、私はこれを完全に一致する2つのパズル・ピース、または互いに補完し合う2つのフレーバーだと考えたいのです。

依然、最初のHTMLの生成にはサーバー・サイド・レンダリングが必要です。React Server Componentsはその上に構築されるため、クライアント側のJavaScriptバンドルから特定のコンポーネントを除外し、それらのコンポーネントがサーバー上でのみ実行されるようにできます。

確かに、React Server Componentsは、サーバー・サイド・レンダリングなしに使うことは可能ですが、実際は両方を一緒に使用するとより良い結果が得られます。ReactチームがSSRを使用しない最小限のRSCデモを作成していますので、こちらでその例を見ることができます。

互換性のある環境

したがって通常は、新しいReact機能が出てきた場合、Reactの依存関係を最新バージョンに更新することで、既存のプロジェクトでその機能を使い始めることが可能になります。迅速なnpm install react@latestによって、すぐに開始できます。

残念ながら、React Server Componentsはそのようには機能しません。

私が理解しているところでは、React Server Componentsは、バンドラーやサーバー、ルーターなど、Reactの外部にある多くのものと緊密に統合する必要があります。

この文を書いている時点で、React Server Componentsの使用を開始する方法は1つだけであり、それはNext.js 13.4以降で、最新の再設計された「App Router」を使用することです。

願わくは今後、より多くのReactベースのフレームワークに、React Server Componentsが組み込まれることを期待しています。Reactのコア機能が特定の1つのツールでしか利用できないことについては抵抗を感じます。Reactドキュメントには「最先端のフレームワーク」セクションがあり、React Server Componentsをサポートするフレームワークがリストされています。新しいオプションが利用可能になったかどうかを確認するため、このページを時々チェックする予定です。

クライアント・コンポーネントの指定

この新しい「React Server Components」パラダイムでは、すべてのコンポーネントが、デフォルトで、サーバー・コンポーネントとみなされます。クライアント・コンポーネントについては、「オプトイン」する必要があります。

それを行うために、まったく新しいディレクティブを指定します。

'use client';
import React from 'react';
function Counter() {
  const [count, setCount] = React.useState(0);
  return (
    <button onClick={() => setCount(count + 1)}>
      Current value: {count}
    </button>
  );
}
export default Counter;

先頭の'use client'ディレクティブは、このファイル内のコンポーネントがクライアント・コンポーネントであることや、クライアントで再レンダリングできるようJSバンドルに含める必要があることをReactに知らせます。

これは、作成しているコンポーネントのタイプを指定する方法として非常に奇妙に見えるかもしれませんが、これには前例があります。それは、JavaScriptで「Strict Mode」を選択する"use strict"ディレクティブです。

サーバー・コンポーネントでは、'use server'ディレクティブを指定しません。React Server Componentsパラダイムでは、コンポーネントは、デフォルトで、サーバー・コンポーネントとして扱われます。事実、'use server'はサーバー・アクションに使用されますが、それはまったく異なる機能であり、このブログ記事の範囲を外れています。

none

クライアント・コンポーネントにすべきコンポーネントはどれでしょうか?

「特定のコンポーネントをサーバー・コンポーネントにするかクライアント・コンポーネントにするかをどのように決定すればよいのか?」と疑問に思われるかもしれません。

一般則としては、コンポーネントがサーバー・コンポーネントになれるのであれば、サーバー・コンポーネントにするべきです。サーバー・コンポーネントはよりシンプルであり、推測が容易になる傾向があります。パフォーマンス上の利点もあります。サーバー・コンポーネントはクライアント上では実行されないため、そのコードはJavaScriptバンドルに含まれません。React Server Componentsパラダイムの利点の1つは、ページ・インタラクティブ(TTI)メトリックを改善できる可能性があることです。

とはいえ、できるだけ多くのクライアント・コンポーネントを排除することを目指すべきではありません。最も少ない数のクライアント・コンポーネントに合わせて最適化を試みるべきではありません。これまでは、すべてのReactアプリのすべてのReactコンポーネントがクライアント・コンポーネントであったことは覚えておくべきです。

React Server Componentsを使い始めれば、それが非常に直感的なものであることが分かるでしょう。一部のコンポーネントはstate変数またはeffectを使用することから、クライアント上で実行する必要があります。そうしたコンポーネントでは、'use client'ディレクティブを実行できます。それ以外の場合は、サーバー・コンポーネントのままにしておくことができます。

クライアント・バウンダリ

React Server Componentsに慣れ始めた頃、私が最初に抱いた疑問の1つが、propsが変更されるとどうなるのか?というものでした。

たとえば、次のようなサーバー・コンポーネントがあったとします。

function HitCounter({ hits }) {
  return (
    <div>
      Number of hits: {hits}
    </div>
  );
}

最初のサーバー・サイド・レンダリングで、hits0だったと仮定します。このコンポーネントは次に、以下のマークアップを生成します。

<div>
  Number of hits: 0
</div>

しかし、hitsの値が変化したらどうなるでしょうか?これがstate変数で、0から1に変化するとします。HitCounterは再レンダリングする必要がありますが、サーバー・コンポーネントであるため再レンダリングはできません

問題は、サーバー・コンポーネントは単独では、事実上意味がないということです。より全体的なビューを取り上げ、アプリケーションの構造を検討するには、ズームアウトする必要があります。

仮に、次のようなコンポーネント・ツリーを考えてみましょう。

(訳注:イメージは原文サイトの表示のキャプチャー画像です)

これらのコンポーネントがすべてサーバー・コンポーネントであれば、完全に意味が通じます。いずれのコンポーネントも再レンダリングされないため、どのpropsも変化しません。

ただし、Articleコンポーネントがhitsというstate変数を持っていると仮定します。stateを使用するには、それをクライアント・コンポーネントに変換する必要があります。

(訳注:イメージは原文サイトの表示のキャプチャー画像です)

ここでの問題が分かりましたか?Articleが再レンダリングされると、HitCounterDiscussionなど、所有していたコンポーネントも再レンダリングされます。ところが、これがサーバー・コンポーネントの場合には、再レンダリングできません

このあり得ない状況を防ぐために、Reactチームは次のルールを追加しました。クライアント・コンポーネントがインポートできるのは他のクライアント・コンポーネントのみである。その'use client'ディレクティブは、HitCounterDiscussionのインスタンスがクライアント・コンポーネントにならなければならないことを意味します。

React Server Componentsに関連して私が経験した最大の「なるほど」の瞬間の1つは、この新しいパラダイムはクライアント・バウンダリの作成に関するものであると認識したときでした。実際には、次のようになります。

(訳注:イメージは原文サイトの表示のキャプチャー画像です)

'use client'ディレクティブをArticleコンポーネントに追加すると、「クライアント・バウンダリ」が作成されます。このバウンダリ内のコンポーネントはすべて、暗黙的にクライアント・コンポーネントに変換されます。HitCounterなどのコンポーネントには'use client'ディレクティブがありませんが、この特定の状況ではクライアント上でハイドレーション/レンダリングが行われます 4

つまり、クライアント上で実行する必要があるすべてのファイルに'use client'を追加する必要はないということです。実際に、追加する必要があるのは、新しいクライアント・バウンダリを作成する場合だけです。

対処法

クライアント・コンポーネントはサーバー・コンポーネントをレンダリングできないということを初めて知ったとき、私はかなり制約がきついなと感じました。アプリケーションの上の方でstateを使用しなければならない場合、どうすればよいのでしょうか?それは、すべてがクライアント・コンポーネントになる必要があるということでしょうか??

多くの場合において、アプリケーションを再構築して、レンダリングとなるコンポーネントを工夫することで、この制約を回避できることが分かりました。

これを説明するのは難しいので、例を使って説明します。

'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>
  );
}

この設定では、React stateを使用して、ユーザーがダーク・モードとライト・モードを切り替えられるようにする必要があります。このことは、CSS変数トークンを<body>タグに適用できるようにするため、アプリケーション・ツリーの上部で行う必要があります。

stateを使用するには、Homepageをクライアント・コンポーネントにする必要があります。これはアプリケーションの最上部にあるため、他のすべてのコンポーネント(HeaderおよびMainContent)も暗黙的にクライアント・コンポーネントになることを意味します。

これを修正するには、カラー管理要素を独自のコンポーネントに引き込み、独自のファイルに移します。

// /components/ColorProvider.js
'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>
  );
}

Homepageに戻り、この新しいコンポーネントを次のように使用します。

// /components/Homepage.js
import Header from './Header';
import MainContent from './MainContent';
import ColorProvider from './ColorProvider';
function Homepage() {
  return (
    <ColorProvider>
      <Header />
      <MainContent />
    </ColorProvider>
  );
}

'use client'ディレクティブは、stateやその他のクライアント側React機能を使用しなくなることから、Homepageから削除できます。これは、HeaderMainContentが暗黙的にクライアント・コンポーネントに変換されなくなることを意味します。

しかし、ちょっと待ってください。クライアント・コンポーネントであるColorProviderは、HeaderとMainContentのです。いずれにせよ、まだツリーでは上位にありますね。

ただし、クライアント・バウンダリに関しては、親子関係は重要ではありません。Homepageは、HeaderMainContentをインポートし、レンダリングするものです。これは、Homepageが、これらのコンポーネントのpropsが何であるかを決定することを意味します。

覚えておいてください。私たちが解決しようとしている問題は、サーバー・コンポーネントは再レンダリングができないため、どのpropsにも新しい値を与えることができないということです。この新しい設定では、Homepageが、HeaderMainContentのpropsを決定し、またHomepageはサーバー・コンポーネントであることから、問題はありません。

これは頭を悩ませる事柄です。何年もReactを経験した後の今でも、このことはきわめて分かりにくいと思っています😅。これに対する直感を養うためには、かなりの練習を必要としました。

より正確に言えば、'use client'ディレクティブはファイル/モジュール・レベルで機能します。クライアント・コンポーネント・ファイルにインポートされたモジュールもクライアント・コンポーネントでなければなりません。結局のところ、バンドラーがコードをバンドルすると、これらのインポートに追随することになります。

none

カラー・テーマを変更するか?

上記の例には、カラー・テーマを変更する方法がないことに気付いたかもしれません。setColorThemeが呼び出されることは決してありません。 可能な限り最小限度に留めたかったので、いくつか省略したものがあります。完全な形の例では、Reactコンテキストを使用して、任意の子孫がsetter関数を使用できるようにします。コンテキストを使用するコンポーネントがクライアント・コンポーネントである限り、すべてうまくいきます。

内部で何が起きているのか

これを、もう少し深く見てみましょう。サーバー・コンポーネントを使用する場合、その出力はどのようなものになるでしょうか?実際に生成されるのは何でしょうか?

ここで、超シンプルなReactアプリケーションから始めることにしましょう。

function Homepage() {
  return (
    <p>
      Hello world!
    </p>
  );
}

React Server Componentsパラダイムでは、デフォルトですべてのコンポーネントがサーバー・コンポーネントです。このコンポーネントをクライアント・コンポーネントとして明示的にマーク付けしていないため(またはクライアント・バウンダリ内でレンダリングしていないため)、サーバー上でのみレンダリングされます。

ブラウザでこのアプリにアクセスすると、次のようなHTMLドキュメントを受け取ることになります。

<!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>
none

勝手な変更

分かりやすくするため、ここであえて再構成を実施しました。たとえば、RSCコンテキストで生成された真のJSは、このHTMLドキュメントのファイル・サイズを削減するための最適化として、文字列化されたJSON配列を使用します。

また、HTMLの重要でない部分(<head>など)もすべて削除しました。

HTMLドキュメントには、Reactアプリケーションによって生成されたUI、「Hello world!」パラグラフが含まれていることが分かります。これはサーバー・サイド・レンダリングのおかげであり、React Server Componentsに直接起因するものではありません。

その下には、JSバンドルをロードする<script>タグがあります。このバンドルには、Reactなどの依存関係と、アプリケーションで使用されるクライアント・コンポーネントが含まれています。また、Homepageコンポーネントはサーバー・コンポーネントであるため、そのコンポーネントのコードはこのバンドルには含まれていません。

最後に、いくつかのインラインJSを含む2番目の<script>タグがあります。

self.__next['$Homepage-1'] = {
  type: 'p',
  props: null,
  children: "Hello world!",
};

これは本当に興味深い点です。本質的には、ここで行っているのは、Reactに対して、「Homepageコンポーネント・コードが足りないことは分かっていますが、心配しないでください。ここにレンダリングされたものがあります」と伝えることです。

通常、Reactがクライアント上でハイドレーションを行うと、すべてのコンポーネントが素早くレンダリングされ、アプリケーションの仮想DOMが構築されます。サーバー・コンポーネントの場合、コードがJSバンドルに含まれていないため、これを行うことができません。

したがって、レンダリングされた値と共に、サーバーによって生成された仮想DOMを送信します。Reactがクライアントにロードされると、そのディスクリプションを再生成する代わりに再利用します。

これにより、上記のColorProviderの例が機能するようになります。HeaderMainContentからの出力は、childrenというpropsを通じてColorProviderコンポーネントに送られます。ColorProviderは必要なだけ再レンダリングを行えますが、このデータは静的であり、サーバーによってロックされています。

サーバー・コンポーネントがどのようにシリアル化され、ネットワーク経由で送信されるかの現実的表現を見たい場合は、開発者であるAlvar LagerlöfによるRSC Devtoolsをチェックしてください。

none

サーバー・コンポーネントはサーバーを必要としない

この記事の始めで、サーバー・サイド・レンダリングは、次のようなさまざまなレンダリング戦略についての「包括的な用語」であると述べました。

  • 静的:HTMLは、展開プロセス中に、アプリケーションのビルド時に生成されます。
  • 動的:ユーザーがページをリクエストすると、HTMLは「オンデマンド」で生成されます。

React Server Componentsは、これらのレンダリング戦略のいずれとも互換性があります。サーバー・コンポーネントがNode.jsランタイム中にレンダリングされると、それらが返すJavaScriptオブジェクトが作成されます。それは、オンデマンドまたはビルド中のいずれかで発生する可能性があります。

つまり、React Server Componentsをサーバーなしで使用できるということです。大量の静的HTMLファイルを生成し、必要な場所にホストすることができます。事実、これがNext.jsのApp Routerにおいて、デフォルトで行われることです。本当に「オンデマンド」で行う必要がない場合、この作業はすべて事前に、ビルド中に実行されます。

none

Reactがまったくない場合?

アプリケーションにクライアント・コンポーネントを含めない場合、本当にReactをダウンロードすることが必要なのかという疑問を持つかもしれません。React Server Componentsを使用して、真に静的な非JSウェブサイトを構築できるでしょうか?

問題は、React Server ComponentsはNext.jsフレームワーク内でのみ利用可能であり、そのフレームワークにはルーティングなどを管理するためにクライアント上で実行する必要のあるコードが多数含まれていることです。

ただし直感とは逆に、このことで実際にはより良いユーザー・エクスペリエンスが実現する傾向があります。たとえば、Nextのルーターは、新しいHTMLドキュメント全体を読み込む必要がないため、典型的な<a>タグよりも速くリンク・クリックを処理できます。

適切に構造化されたNext.jsアプリケーションは、JSのダウンロード中も動作しますが、JSが読み込まれるとさらに速く/良くなります。

利点

React Server Componentsは、Reactでサーバー専用コードを実行することを目的とした最初の「公式」ツールです。ただし、すでに述べたように、より広範なReactエコシステムにおいては、これは決して新しいことではありません。2016年から、Next.jsではサーバー専用コードを実行できるようになっていました。

大きく異なるのは、これまでコンポーネント内でサーバー専用コードを実行する手段がなかった点です。

最も明白な利点はパフォーマンスです。JSバンドルにはサーバー・コンポーネントが含まれていないため、ダウンロードすべきJavaScriptの量と、ハイドレーションを必要とするコンポーネントの数が減ることになります。

(訳注:イメージは原文サイトの表示のキャプチャー画像です)

ただし、このことは私にとっては最も興味を感じられないものかもしれません。正直なところ、「ページ・インタラクティブ」のタイミングに関しては、ほとんどのNext.jsアプリがすでに十分な高速度を実現しています。

セマンティックHTMLの原則に従えば、Reactのハイドレーション前でも大半のアプリが動作するはずです。リンクをたどったり、フォームを送信したり、(<details><summary>を使用して)アコーディオンを展開したり折りたたんだりできます。ほとんどのプロジェクトで、Reactのハイドレーションに数秒かかっても、問題はありません。

ただし、私が本当にすごいと思うのは、機能とバンドル・サイズという点で、同じ妥協をする必要がなくなったことです。

たとえば、ほとんどの技術系ブログでは、ある種のシンタックス・ハイライティング・ライブラリが必要となります。このブログでは、Prismを使用します。コード・スニペットは次のようになります。

function exampleJavaScriptFunction(param) {
  return "Hello world!"
}

一般的なプログラミング言語のすべてをサポートする適切なシンタックス・ハイライティング・ライブラリは数メガバイトになり、JSバンドルに組み込むには大きすぎます。そのため、ミッションクリティカルでない言語や機能を削除するという妥協が必要になります。

ただし、シンタックス・ハイライティングを行うのはサーバー・コンポーネントにおいてであると仮定しています。その場合、実際にはライブラリ・コードはJSバンドルに含まれません。その結果、妥協する必要が一切なくなり、オプション機能のすべてを利用できるようになります。

これが、React Server Componentsで動作するよう設計された最新のシンタックス・ハイライティング・パッケージであるBrightの背後にある卓越したアイデアです。

これこそが、React Server Componentsについて私が興味をそそられる点です。JSバンドルに含めるにはコストがかかりすぎることが、サーバー上で、無料で実行できるようになり、バンドルに追加される容量は0キロバイトで、これまで以上に優れたユーザー・エクスペリエンスを得ることができます。

単にパフォーマンスの問題でもUXの問題でもありません。RSCをしばらく使ったことで、私はサーバー・コンポーネントがどれほど簡単であるかを真に理解するようになりました。ディペンデンシー・アレイ、古くなったクロージャ、メモ化、状況の変化によって引き起こされるその他の複雑な事柄について心配する必要はありません。

結局のところ、それはまだきわめて初期の段階にあります。React Server Componentsは数か月前に、ベータ版が完了して正式リリースになったばかりです。コミュニティがこの新しいパラダイムを活用して、Brightなどの新しいソリューションのイノベーションを続けていることから、今後数年間でどのように進化するかを見るのが本当に楽しみです。React開発者にとって今は、エキサイティングな時です。

全体像

React Server Componentsはエキサイティングな発展ですが、実際には「Modern React」パズルの一部にすぎません。

事態が真に興味深いものになるのは、React Server ComponentsをSuspenseや新しいStreaming SSRアーキテクチャと組み合わせたときです。その結果、次のような驚くべきことが可能になります。

このチュートリアルの対象範囲を超えていますが、このアーキテクチャの詳細については、Githubで確認することができます。

また、私の新しいコースThe Joy of Reactでも詳しく取り上げます。よろしければ、もう少し詳しく説明させていただきたいと思います!❤️

The Joy of Reactは初心者向けのインタラクティブ・コースで、Reactがどのように機能するかについての知識を得られる設定になっています。初歩段階からスタートし(Reactの経験は必要ありません)、Reactの悪名の高いトリッキーな側面を取り上げていきます。

これは私が約2年間、フルタイムで取り組んできたコースであり、8年以上の経験の中でReactについて学んだ最も重要な事柄のすべてが含まれています。

お伝えたしたいことが満載のコースです。React本体と、このブログ記事で暗に取り上げた最先端の機能に加えて、Reactエコシステムの中で私が気に入っている部分についても学ぶことができます。たとえば、Framer Motionを使用して、次のレベルのレイアウト・アニメーションを作成する方法を学べます。

このコースについてさらに詳しく学び、Reactを使ったビルドの楽しさを発見してください。


React Server Componentsは重要なパラダイム・シフトです。個人的には、Reactエコシステムにおいて、サーバー・コンポーネントを利用するBrightのようなツールがさらに構築されるにつれて、今後数年間で状況がどのように展開していくかに関してきわめて強い関心があります。

Reactでの構築がさらにすごいことになると感じています。😄


  1. 特定のモジュールをレイジーロードしたり、ルートに基づいて分割したりするような最適化もありますが、一般的なルールとして、JSバンドルはどんどん大きくなる傾向があります

  2. メタフレームワークとは、Reactの上に構築し、ルーティングやデータ管理のような機能を追加するフレームワークを意味する

  3. 少なくとも、新しいページに移動するなど、ルーター上で何かが起こるまでは

  4. HitCounterコンポーネント自体は、サーバー・コンポーネントにより他の場所にインポートされた場合、サーバー・コンポーネントとしてレンダリングされる可能性があります