やっと React Server Components が腑に落ちた

2020 年末に発表された [React Server Components](https://ja.reactjs.org/blog/2020/12/21/data-fetching-with-react-server-components.html) は、一言でいうと **React コンポーネントをサーバ側でレンダーする仕組み**です。……が、初見ではちょっと魔法すぎて訳が分からない技術でもあります。[トーク動画](https://www.youtube.com/watch?v=TQQPAU21ZUw)だけ見てても「具体的にどんな仕組みで動いてるの？」みたいな疑問が山ほど浮かんでくると思います。

一体裏で何がどうなっているのか、この技術はどう使うのか、[デモコード](https://github.com/reactjs/server-components-demo)を触りながらやっと具体的に理解しはじめたので、なるべく動作が想像しやすいようにまとめました。なお、この記事単体よりは、他の記事や上記動画を見てある程度概要や公式の売り文句を掴んでからの方が理解しやすいと思います。

これまでの、普通にブラウザで動作するコンポーネントのことを、区別のためにクライアントコンポーネントと呼びます（単に区別のために既存のコンポーネントに別名がついただけです）。以下ではサーバコンポーネントを **SC**、クライアントコンポーネントを **CC** と略します。

## ブラウザから見た SC とは、単に仮想 DOM を返す HTTP リクエストである

CC を一言で説明すると「props を渡すと仮想 DOM が返ってくる、React ツリーにマウントできる JavaScript 関数」のことですね（話を簡単にするためクラスコンポーネントの話は忘れます。以下同様）。

クライアントから見た場合の SC を同じノリで表現すると、「**パラメータを付けて HTTP コールすると仮想 DOM（をシリアライズしたもの）が返ってくる、React ツリーにマウント(?)できる URL**」となります。クライアントが props を直に渡すことはなく、URL 内のパスやクエリ文字列がそれに対応するものになります。仮想 DOM は、以下のような JSON に似たテキストベースの非公開プロトコルでサーバから返ってくるので、クライアント側でそれをパースして処理します。

![HTTP レスポンス](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61702/860837b9-ada6-fa99-c34b-2e6b3549a58e.png)

サーバ側から見た SC とは、これまで同様の props を受け取る JavaScript 関数です。サーバ（デモでは Express）で props に渡すデータを URL から抽出し、props を付けて SC の関数本体を呼び出し、その結果を上のようなものにシリアライズして返却しています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61702/ae0dda42-2b46-b3b8-6b8a-3943c3f251e1.png)

これが「サーバでコンポーネントを実行する」の意味です。React のいわゆる DOM レンダラ・差分検出アルゴリズム（リコンサイラ）の立場まで来れば、CC と SC の間に本質的な違いはありません。出所の違いはあれ、得られる結果は同じ仮想 DOM なので、そこから同じように実 DOM を構築し、同じように差分検出して更新することができます。

**Zero-bundle-size** というキャッチフレーズから分かる通り、SC とそれが `import` しているあらゆるものはバンドルに一切含まれませんし、SC をブラウザ側で `import` することはできません。当然ブラウザ側で `<SomeServerComponent />` のような形で直接呼び出すこともできません。あくまで何らかの URL をコールすることが SC の呼び出し結果に対応しています。以下がクライアントが SC を使うための最低限のコードです。

```jsx:クライアントからSCは以下のようにして呼び出す
import { createFromFetch } from "react-server-dom-webpack";
import ReactDOM from 'react-dom';

const App = () => { // このApp自体はCC
  const [response, setResponse] = useState(undefined);
  useEffect(() => setResponse(fetch("/react")), []);

  return response ? <>{createFromFetch(response).readRoot()}</> : null;
};

ReactDOM
  .unstable_createRoot(document.getElementById('root'))
  .render(<App />);
```

基本的な仕組みは単純で、`createFromFetch` が fetch の結果（シリアライズされた仮想 DOM）を本物の React 要素（仮想 DOM）に変換しているだけということが分かりますね。実際のデモではここにキャッシュやカスタムフックが挟まっておりもう少し複雑で、アプリの状況に応じて上記のような SC の再レンダー（再 HTTP リクエスト）が複数回発生します。

目ざとい人は 1 行目になんか `webpack` という文字列が見えるのが気になるかもしれませんが、この点については後で述べます。

やや面倒そう……というか、**「React コンポーネントを使ってる」感は皆無**ですね。でもこれは意図的にそうしているようです。その気になればクライアント側でこれらの URL や HTTP コールを隠蔽するラッパーや高階コンポーネントは書けますし、一旦そうしてしまえばこれまでのコンポーネントと一見変わらない雰囲気で扱うこともできるでしょう。しかしそういう気軽な使い方はあまり推奨されていないようです。これも後述します。

ここまでの話を一言でまとめると、クライアント側から見た SC とは「コンポーネントツリーの好きな場所で、**URL をコンポーネントに見立ててマウントする仕組み**」です。

## SC ではいたるとろで非同期処理にサスペンスを使う

ご存知の通り React コンポーネントは絶対に async にできません。props を受け取ったら**同期的に**仮想 DOM を返さないといけない、という鋼の掟が存在しています。上記のコードにも `async`/`await` の文字はどこにもありません。その一方で SC では仮想 DOM がネットワークを流れるわけで、どう考えても非同期じゃないと困ります。しかしこれらは同一ツリー内で平然と混在させることができます。

これを実現するために使われるのが「[サスペンス (Suspense)](https://ja.reactjs.org/docs/concurrent-mode-suspense.html)」、すなわち **"Now Loading" を宣言的に（JSX で）書く**ための公式の仕組みです。非同期データの扱いは React の一大テーマでしたが、サスペンスこそが、リソースやコード取得周りの非同期処理に関して今後主流になると期待されているものです。これ自体は SC 以前からある仕組みです。

サスペンスを使うと、元来同期的なものである React コンポーネントツリーのレンダー処理の途中で、まだ仮想 DOM を出力する準備のできていない部分が「こちらの準備が整うまで**待った**」という信号をエラーの形で送れるようになり、それに応じたローディング画面を**宣言的に JSX 上で**指定できるようになります。具体的には JavaScript の `throw` を使って Promise を投げることでレンダー処理を中断します。詳細は他の方の解説に譲ります。

https://sbfl.net/blog/2020/02/10/react-suspense-async/

`React.lazy` 以外でのサスペンスの使用はまだ experimental ですが、SC を扱う場合サスペンスの知識は絶対条件であり、これを知らないとデータロードひとつ行えません。以下の説明でもサスペンスの知識を前提としますので、知らない方は上記のリンクで先に学んでください。

::: note

ところで、この記事の本題とは全く関係ないのですが、サスペンスや `useState` の仕組みが汚いとか邪道だとか感じている人は、[React チームの Dan Abramov 氏（上記トークで喋ってる人）によるこちらの記事](https://overreacted.io/ja/algebraic-effects-for-the-rest-of-us/)をお勧めします。リンク先は和訳です。これらのアイディアは邪道どころか、関数型言語の未来から来ているということが分かりやすく説明されています。

:::

## SC からの応答はストリーム化されている

サスペンスが偉いのは、クライアントから SC を呼び出すところだけではなく、**SC 自体の内側**でもシームレスに動作するところです。以下の例をご覧ください。

![Server-side Suspense](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61702/2953537b-0ca5-30f7-bcab-8710ec6a9366.gif)

これは、クライアントから見ればただ 1 つの SC である `App`（に対応する URL）をただ 1 回レンダーしている例です。クライアント側コードに状態管理や `<Suspense>` は一切書いていません。が、見ての通り段階的なローディングが実現できています。サーバ側のコードは以下のような感じです。`AlbumInfo` 内に人工的な遅延（サスペンド）を入れています。

```jsx:これは「サーバ側の」コードです！
import AlbumInfo from './AlbumInfo.server'; // 直接DBにアクセスするSC

export default function App() {
  const albumIds = ['apple', 'avocado', 'banana'];
  return (
    <div>
      <h1>Hello, Server World!</h1>
      <div className="albums">
        {albumIds.map((id) => (
          <Suspense fallback={<div>Loading "{id}"...</div >}>
            <AlbumInfo albumId={id} />
          </Suspense>
        ))}
      </div>
    </div>
  );
}
```

これが可能なのは、App の返り値、つまり HTTP レスポンスがストリームとして処理されているからです。1 つの HTTP レスポンス内で、先にフォールバックが（仮想 DOM で）送られてきて、後からその「穴」を埋めるようにして結果本体が（仮想 DOM で）やってきます。全部のサスペンスがアンロックされたところで HTTP リクエストがめでたく終了となります。

最近 React チームは非同期レンダリングとか並行モード周りにやたら力を入れていましたが、「上級者にしか関係のないマニアックな最適化の話」「redux-thunk とか `useEffect` とかの使い慣れた自前のデータロードの仕組みで十分」と思っていた人も、正直多いのではないでしょうか。しかし「ロード中状態」の公式なサポートを React が拡張しようとしている背景には、このようなサーバ側での使い道まで含まれていたのね、というのが分かります。これは確かに、非同期データ取得をすべて開発者の自前処理に任せていたのでは実現できなかったでしょう。

:::note
**2022年4月追記**：このサーバ側でのサスペンス対応は、SC より先にサーバサイドレンダリング (SSR) で React 18.0 より本番利用可能になりました。SSR 結果の HTML 中にインラインの `<script>` タグが含まれており、React 本体がロードされる前に「穴」を埋めてくれるようです。React 18.0 リリース時点で SC 自体はまだ開発途上というステータスです。
:::

## SC から CC の呼び出しはシームレスにできる

ここまでの説明で可能になったことは大したことではありません。ぶっちゃけ、サーバ側で適当なテンプレートライブラリで作った HTML 文字列をクライアント側で fetch して element.innerHTML にセットすれば、React なんてなくてもほぼ同じことができます。が、この辺からちょっと魔法じみてきます。

上記の通り、クライアント側で SC 本体を直接 `import` してレンダーすることはできず、あくまで URL を経由することになります。しかしその逆は可能です。サーバ側では、任意の CC を単に `import` してレンダー結果に含めることができます。SC 内で CC を「使う」と、その CC が~~魔法により~~**ブラウザ側で**マウントされ、その中の `useState` や `useEffect` が普通に動作します。（なお SC の関数本体内では `useState`、`useReducer` や `useEffect` は一切使えません。書いても無視されるとかではなく、書くことすら許されません。） 言い換えると、SC によるサーバ側からの指示により、**ブラウザ側で任意の CC をマウントさせ**、そこであらゆるインタラクティブなことをさせられるというわけです。

以下は、`useState` や `useEffect` を使った CC を SC のレンダー結果に含めているシンプルな例です（CC である `Clock` は自己を更新する時計であり、内部に state を持っています。コードは自明なので省略。以下同様）。

```jsx:ステートフルな時計を表示
import Clock from "./Clock.client"; // CCをインポート

export default function App() { // これはSC
  return (
    <div>
      <h1>Hello, Server World!</h1>
      <Clock format="YYMMDD hh:mm:ss" />{/* ←これはCC! */}
    </div>
  );
}
```

実行結果：
![Embedded CC](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61702/8984b0a3-50e1-3494-d5f2-7c847d049357.png)

もうすこし凝った例として、CC の子要素 (children) や props にサーバ側でレンダーしたコンテンツを渡すこともできます。

```jsx:ステートフルなアコーディオン内にSCを使う
import Accordion from "./Accordion.client";
import AlbumInfo from "./AlbumInfo.server";

export default function App() { // これはSC
  return (
    <div>
      <Accordion title="First Stage">なんか長文</Accordion>
      <Accordion title="詳細">{/* これはCC */}
        <AlbumInfo id="grape">{/* これはSC */}
      </Accordion>
    </div>
  );
}
```

なお、上記はある意味で「CC 内に SC をネストしている」例ですが、CC は `props.children` に渡される仮想 DOM をそのまま出力しているだけであり、CC の実装自体が何か SC を `import` したり呼び出したりしているわけではないので、前述の制限には該当しません。この辺混乱しがちなので要注意です。

さらには、サーバ側のサスペンスのフォールバックとして CC を使うことも可能です。

```jsx:フォールバックにCCを使う
import LoadingIndicator from "./LoadingIndicator.client";

export default function App() { // これはSC
  const albumIds = ["apple", "avocado", "banana"];
  return (
    <div>
      <h1>Hello, Server World!</h1>
      <div className="albums">
        {albumIds.map((id) => (
          <Suspense fallback={<LoadingIndicator />}>{/* フォールバックはCC */}
            <AlbumInfo albumId={id} />{/* これはSC */}
          </Suspense>
        ))}
      </div>
    </div>
  );
}
```

実行結果：
![Server-side Suspense with Client-side Fallback](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61702/6fb336b9-4f07-5b73-1ec4-304c3e8cf63d.gif)

CC はサーバ側では実行されず、代わりに SC の HTTP レスポンス内で名前で参照され、ブラウザ側でマウントされてレンダーされます。親である SC の再レンダー（＝HTTP の再リクエスト）によって CC 側の state やフォーカスやアニメーションが吹き飛ばされる心配はもちろんありません。この、state を保持したまま好きに CC を取り回せる点が、SSR や単なる静的なテンプレートとの最大の違いです。

ところで、上のほうで紹介した SC を URL でマウント(?)するコードに `'react-server-dom-webpack'` という謎のモジュールがありました。あそこで突然 Webpack の名が登場するのは、SC がレスポンス内で参照している CC、つまり上記の `Clock` や `Accordion` や `LoadingIndicator` がバンドル内に既にあるとは限らないので、それを何とかするのに Webpack の助けが必要だからです。SC は HTTP レスポンス中で、ブラウザ側にとって未知の CC を返しても構いません。その場合、「足りない CC がある」ということを上記のモジュールが検出し、Webpack の仕組みを使ってそのモジュールを自動で遅延ロードしてくれます（当然サスペンスがここでも使われます）。自前で `React.lazy` しなくても構いません。

:::note

基本事項ではあるのですが、コンポーネント内で `return <Clock />` と書いたとき、コンポーネントはその場で `Clock()` を呼び出しているのではなく「**必要な時が来たら**ここで `Clock` を呼び出してね」と、React に関数参照を使って指示を出しているのだ、と理解しましょう。いつ実際に `Clock` が呼び出されるのか（あるいはそもそも呼び出さないのか）については、React 側に決定権があります。特に最近の React の新機能群を理解するには、この認識が重要です。SC の場合「`Clock` を呼んでね」という指示を出すのはサーバ側で、実際に実行されるのはフロントエンド側のデバイスにロードされた `Clock` 関数です。関数本体のコードと props をクライアントに届ける方法さえあればこれは実現可能であり、それをやっているのが SC ということです。

:::

## SC と CC の混在方法についての注意

ここまでを簡単にまとめます。

- ブラウザ内で SC を呼び出す際は、特別な API を使って『URL をマウント』する。
- サーバ内で CC を呼び出す際は、単に `import` して `<SomeClientComponent />` のように使う。

前者だけが特殊で面倒な形式なのは、**前者のようなことは気軽にやるなよ**という React チームの意思表示だと思います。RFC でも前者をもっと「自然」にするための提案は全部拒否されています。一方、後者はやればやるほど SC のパフォーマンスメリットが活かされます。

クライアントからの SC の呼び出しとはすなわち HTTP リクエストの発行であり、これは CC のレンダー（ローカル関数のコール）と比べると気が遠くなるくらい遅い処理です。それを小さな粒度で気軽にやられるとパフォーマンスに悪影響が出ます。というか、小さな粒度でクライアントから SC を呼び出していたのでは、従来の useEffect で数珠つなぎに REST API を呼ぶような仕組みと比べてパフォーマンス面での**メリットが皆無**になってしまいます。すなわち、**SC と CC を複雑に混在させることは可能ですが、それはサーバ内で、それも可能な限りコンポーネントツリーの上の方の SC でまとめて行うべき**ということです。そうやって、1 リクエストで複雑なコンポーネントツリー（のレンダー結果）をまるごと返すのです。

クライアントから SC へのエントリポイント（『URL のマウント』）は、技術的にはどこにでも何回でも作成できますが、推奨されるのは「ページ」などの大きな単位で 1 回のみ作成することです。この意味では、SC という技術を「API + マイクロテンプレート」のように考えるのではなく、**SPA 以前の時代のページまるごとのテンプレートに役割が近い**と考えていた方が良さそうです（もちろん前述の通り、進化はしていますが）。全体的なページのレイアウトや固定の見出しタグの表示みたいなものすら SC の配下でやるべき仕事です。こうできない場合、そもそも SC という技術を使わず普通に API を使った方がよい可能性が高いと思います。

絵で示すと、左は SC へのエントリポイント（赤丸）が 1 つなので「良い」SC の使い方、右はエントリポイントがたくさんあるので「悪い」SC の使い方になります。

![Mixing SCs and CCs](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61702/d03694d4-20f1-3c7f-4177-f707f80580fc.png)

デモリポジトリでも『URL をマウント』するための `Root` コンポーネントが SC への唯一のエントリポイントとなっており、すべてのコンテンツが間接的に SC の配下でレンダーされ、CC はインタラクティブ性が必要なところだけで使われています。

とはいえ SC の再レンダー（HTTP リクエスト）間で変化しない固定の見出しやレイアウト部分まで毎回転送されてくると非効率なので、部分的な更新の仕組みが今後入るかもしれませんが、そこら辺は仕様の研究途中のようです。例えば 2 回目以降の HTTP リクエストが前回のリクエストからの（仮想 DOM 的な）差分を返す、みたいなことは将来的にできるかもしれません。

## SC と CC のディレクトリを分けない

::: note alert

**2022年8月追記**: このセクションの記載は古くなっています。結局コンポーネントの種別は[ソースコード内で注記する](https://github.com/reactjs/rfcs/pull/189#issuecomment-1116482278)方向になるようです。

:::

SC と CC の区別はファイル名で行われます。`*.server.js` は SC、`*.client.js` は CC です。

SC の仕組みを API や旧来のテンプレートの亜種として考えてしまうと、SC を `src/server` みたいなディレクトリ、あるいは別リポジトリに配置したくなるかもしれませんが、恐らくそれは良くありません。React チームはコンポーネントを置くディレクトリを**サーバかクライアントかという基準で分けるのではなく**、画面/機能別（「アーティスト情報」「設定画面」等）に分けさせたいようです。そうでなければ、そもそも `.server.js`/`.client.js` などという命名規約が要りませんよね。デモでも SC と CC は同じディレクトリに混在しており、`onClick` とか書いてあるファイルの隣に `select * from notes` とか書いてあるファイルが堂々と置いてあります。

これに最初抵抗がある人は多いと思います。しかし**サーバとクライアント、フロントエンドとバックエンドの境界を曖昧にし、行き来のコストを減らす**のがそもそもこの技術の目的なので、こうするのが必然であるとも思います。これが納得できないなら SC という技術の魅力が半減してしまうので黙って GraphQL 使っとけ、という話ですね。

ファイル名に `server` も `client` も付けない場合、それは共有コンポーネントになります。例えば特にステートを持たず、単にデータを `<table>` にレンダーしているだけのコンポーネントは、サーバでもクライアントでも動くので共有コンポーネントです。これらは、SC 内で使われた場合サーバ内で `<table><tr>...</tr></table>` タグのレベルまでレンダーされ、CC 内で使用されれば従来の普通のコンポーネントとしてクライアント内でレンダーされます。最終的な見た目の結果は変わらなくともパフォーマンス特性が変わってきます。

## SC がローカルリソースにアクセスする場合にサスペンス互換 API が必要

::: note alert

**2022年8月追記**: このセクションの記載は古くなっています。安定リリース時には結局サーバ側では async/await が使えるようになる模様です。まあそりゃそっちの方が便利ですね。

:::

SC といえども `async` にすることはできません。以下のような Promise を直接使うコードは例えサーバであっても動作しません。

```jsx:このコードは動作しない
import { promises as fs } from "fs"; // あるいは `fs-extra` のような Promsie 互換 fs

const MyServerComponent = async () => {
  const text = await fs.readFile("data.txt", "utf8");
  return <pre>{text}</pre>;
};
```

SC には `useState` や `useEffect` も存在しないので、エフェクト内でデータを読み込むようなテクニックも使えません。代わりに以下のようにする必要があります。`async` や `await` キーワードが消えていることに注意してください。

```jsx:修正版
import fs from "react-fs"; // ←なにこれ？

const MyServerComponent = () => {
  const text = fs.readFile("data.txt", "utf8");
  return <pre>{text}</pre>;
};
```

一見、完全に同期的にファイルを読みだしているかのように見えますがそうではありません。上記で出てきた `react-fs` とは `fs` モジュールをラップしたもので、要するに async 型の API を**サスペンス互換 API に変換**したものです。この新しい `readFile` は、1 回目のコールでは Promise をスローし、それが resolve した後の 2 回目のコールではファイルの読み取り結果を同期的に返します。「`readFile` 自体がキャッシュになっており、キャッシュにヒットすれば同期的に結果が返るが、さもなくば Promise がスローされる」のように考えるとしっくりくるかもしれません。

サーバ側でデータを読み込むなどの非同期処理が必要な場合、常にこのように、Promise (async/await) 型の API をサスペンス型の API に変換したものを使うことになります。やっと世の中のライブラリが概ね Promise 対応になったところでまた面倒な……と思われるかもしれませんが、深淵な思想あってのことであり、任意の Promise をサスペンス型にラップする手法も存在しますので、個人的には深刻な問題にはならないと思います。

## SC を挟んで関数を渡せない

::: note alert

**2023年6月追記**: このセクションの記載も少し古くなったようです。「引数も返り値もシリアライズ可能な関数」に関しては SC から CC に[渡せる](https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions)ようです（でも RFC どこ？）。もっとも、SC で DOM 操作や `setState` なんかは当然不可能ですから、フォームデータを送ってサーバ側でデータベースに保存するための「ハンドラ」関数を送る、みたいな使い方になります。

:::

サーバ側で SC から CC をレンダーできますが「props として SC から CC に関数を渡せない」という重要な制約があります。props がネットワークを経由するんだから当然ですね。`<SomeClientComp onClick={() => alert('Hi')} />` のようなことをサーバでやると以下のような実行時エラーとなります。

```
Error: Event handlers cannot be passed to client component props. Remove onClick from these props if possible: {onClick: function}
If you need interactivity, consider converting part of this to a client component.
```

つまり `onXXX` を props 経由でバケツリレーするような古き良き手法は使えません。以下のような状況で CC である A から B にイベントハンドラを渡したい、という場合は、自前でコンテクストを使う（動作確認はしましたが長すぎてここにコード貼りたくないレベル）か、Redux などで間接的にコンテクストを使うか、そもそも Y を SC にするのを諦めることになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61702/88bc99a8-676d-f2bf-bf68-91dbc7a14267.png)

## SC の使い道とは

以上、長くなりましたが、自分が理解した SC の概要はこんなところです。SC のメリットはいろんなところで語られていますが、以下のようなものです。

- **API をそもそも作らなくてよくなる（かも）**。生 SQL やファイルシステムを直接叩きながら DOM をレンダーできるので、そもそも動的に何かを表示するためにいちいち API の準備自体やらないよ、GraphQL なんか要らないよ、という選択肢が生まれます。

- **応答が速くなる**。ネストされたコンポーネントが順番にマウントされるごとに API が数珠つなぎに呼ばれることによるウォーターフォール（滝）の問題が軽減されます。SC を使ってもウォーターフォールが消えるわけではありません（ウォーターフォールがサーバ側に移動するだけです）が、サーバ側で処理すればこのような場合の応答時間を大幅に減らせることが期待できます。ただし、繰り返しになりますが、このメリットを得るためには、できるだけ SC へのエントリポイントを上にし、ページの大部分を SC でレンダーすることが必要です。

- **バンドルサイズを小さくできる**。サーバ側に Markdown パーサなどの大きなライブラリを移せるのは勿論ですが、もし GraphQL クライアントや状態管理ライブラリまで消せるならその利点も大きいでしょう。

逆に、個人的に SC の弱点/懸念点だなと思うのは以下のようなところです。

- **クライアント側で生データを持てない**。これまでサーバからの応答結果は一旦クライアント側で綺麗にメモリ（Redux ストアでもなんでも）に格納してからレンダーしていましたし、それによりクライアント側で実現できることが色々ありました。が、SC からの結果は React が直接 DOM に反映してしまうので、開発者は中身を確認できません。例えば日付を SC でフォーマットしたはいいけど、日付順にソートする都合で UNIX タイムスタンプ形式でも日付データが必要だ、みたいな場合、困ったことになります。自明な解決法はサーバ側でソートする（≒URL に状態を移動する）ことですが、遅くなります。SC のレスポンスにクライアント側のユーザコードが消費する用のデータをサイドチャンネルで送るような提案がなされていますが、実現するかは不透明です。

- **サーバ側からのプッシュがない**。（サスペンスを除けば）サーバから遅れてデータを送りつけるような仕組みは何もなく、SC を更新するには HTTP リクエストを発行しなおすしかありません。チャットアプリなどでは素直に WebSocket で発言データを送ってもらってクライアントのストアに入れる方がパフォーマンス面で有利です。

- **データ更新用などで結局 API は要る**。SC は表示特化の技術であり、ユーザから何かデータを送るような部分に関しては何もしてくれません。また、サービスが成長してアプリ版も作るなどとなればやっぱり API にしておけば…となるかもしれません。

- **バンドルサイズ減らしたいだけなら既存の方法でも大体できる**。ページ（ルート）毎にコンポーネントを遅延ロードするのは既存の技術です。重いライブラリの代表例として Moment.js が挙げられていましたが、date-fns のような tree shaking が効く軽い代替を使えば今どきは大概解決します。Markdown など替えがきかない激重モジュールが稀にあるならその部分だけ API に逃がして `dangerouslySetInnerHtml` するとかでも大体問題ない気はします。

- **移行に大きめのリファクタリングが要る**。SC に特有の制約を避けるため、状態をどこに保存するか、props のバケツリレーをどう避けるか、といった部分で大抵の場合は面倒なリファクタリングが要ると思います。

正直に言って、SC という仕組みの使いどころは割とニッチだと思います。「SC の方が常にハイパフォーマンス」みたいな言い方をしている記事も見かけますが、SC がパフォーマンスに効くのは巨大ライブラリによるバンドルサイズ肥大やウォーターフォールがボトルネックになっている場合に限られます。SC は React 本体がロードされた後に動く技術ですので、first paint の速度という意味なら SSR や静的サイトジェネレータ系の技術には逆立ちしても勝てっこありません。一方で Redux などを使ったフル SPA と比べると柔軟性や開発者体験で負けており、慣れ親しんだ状態管理やデータフローを再考しないといけません。それらの中間部分で威力を発揮する場面はあるとは思いますが、メリットと特性をよく理解した上で採用しないと苦労しそうです。ここまで書いといてなんですが、個人的なことを言うと、自分が今触っているプロジェクトはクライアント側で複雑な処理がゴテゴテにある SPA なので、当面 SC は要らないかなーと判断しています。

多分「API の設計とかするほどではなく、フロント側でさほど複雑な処理もしないが、ページ表示を素早くしたい」というサービスでは有用なのではないでしょうか。ブログやニュースサイト、あるいはそれこそ Qiita みたいなサービス（から API を無くしたもの）で一番ハマるかもしれません。