🙆

クライアントコンポーネントも SSR される

2024/08/24に公開

はじめに

クライアントコンポーネントも SSR されることを挙動を確認しながら見ていきます。記事の内容としては、当たり前やろ、という感じなので真新しい発見を求めている方はブラウザバックしてください。

確認環境

nextjs の 13.5.6 と 15.0.0-rc.0 で挙動を確認しています。

モチベーション

Nextjs の App Router で、現時点の最新版の Chakra UI (v2) を使用している場合は、すべてクライアントコンポーネントとして扱われます。Chakra UI に限らず、ランタイム css-in-js の場合はすべからく同じだと思います。

all Chakra UI components will be compatible with React Server Components and SSR'd correctly without needed to use the CacheProvider or client directive.
https://www.adebayosegun.com/blog/chakra-panda-ark-whats-the-plan

それについて Chakra UI の作者はブログで、上記のように記載しており、クライアントコンポーネントも正しく SSR されると記載しています。

これを見た時に、あれクライアントコンポーネントって SSR されるっけ...と2秒だけ悩んでしまいました。考えれば、当たり前ではあるのですが、挙動を確認して、記憶に刻んでおくことが今回のモチベーションです。

さっそく本題

以下のように useState を使用しているクライアントコンポーネントは SSR されます。SSR では useState の初期値が使用され、今回の場合は 0 が使用されます。

"use client"
import { useState } from "react";

export default function ClientComponent() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>
        button
      </button>
      <div>
          {`Client Component: ${count}`}
      </div>
    </div>
  );
}

クライアントコンポーネントがブラウザに表示される流れは以下で、SSR と同一のステップです。

1) サーバーサイドレンダリング
  サーバー上でReactコンポーネントがレンダリングされ、HTMLとして送信されます。

2) 初期HTML配信
  ブラウザがサーバーからレンダリング済みのHTMLを受け取ります。

3) JavaScriptのロード
  ブラウザがReactのJavaScriptコードをロードします。

4) ハイドレーション

サンプル実装をブラウザで確認すると以下になりますが、html は SSR での実行結果を使用しており、それに対してハイドレーションすることでレンダリングしています。

ちなみに、Next.js のビルド結果を確認することでも、クライアントコンポーネントが SSR されていることは確認することができます。今回のサンプル実装の場合は、.next/server/app/index.html でコンポーネントのビルド結果を確認することができます。以下の通り、useState の初期値である 0 が使用されて、html が生成されています。

<div><button>button</button>
    <div>Client Component: 0</div>
</div>

useEffect は SSR では実行されない

先ほどのサンプル実装に useEffect を追加して、副作用の中でカウントを +1 しています。

"use client"
import { useEffect, useState } from "react";

export default function ClientComponent() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    setCount(count + 1)
  }, [])


  return (
    <div>
      <button onClick={() => setCount(count + 1)}>
        button
      </button>
      <div>
          {`Client Component: ${count}`}
      </div>
    </div>
  );
}

先ほどど同じく、.next/server/app/index.html でコンポーネントのビルド結果を確認しても、useState の初期値である 0 が使用されて、html が生成されています。useEffect が実行されていれば 1 になっているはずです。

<div><button>button</button>
    <div>Client Component: 0</div>
</div>

useEffect 付きサンプル実装を画面で表示すると以下のようにカウントが +1 されています。

useEffect はクライアント側でのみ実行されていることがわかります。これは useEffect がコンポーネントのライフサイクルにおいてレンダリング後に実行されるため、SSR では実行対象外であると理解しています。

クライアントコンポーネントという名前について

The name “Client Component” implies that these components only render on the client, but that's not actually true.
https://www.joshwcomeau.com/react/server-components/#introduction-to-react-server-components-3

上記、引用にはなりますが、クライアントコンポーネントはクライアント側だけでレンダリングされるわけではありません。名前的にはクライアントのみでレンダリングされそうですが...クライアント側でもサーバー側でもレンダリングされます。先の引用のブログの方は、クライアントコンポーネントという名前が好きではない、という趣旨のことをおっしゃっていますが、確かに名前はミスリードな感じはします。

ちなみに、サーバーコンポーネントはサーバー上でのみレンダリングされます。クライアント側でリレンダリングされることもありません。サーバーコンポーネントは名前通りです。

まとめ

ややこしいです。以上です、終わり。

Discussion