本文へスキップ
サーバーで動くコンポーネント

Part 9 · state を共有する

47 / 64

サーバーで動くコンポーネント

"use client" の 1 行が何を分けているのか

ここまでずっと、コンポーネント=ブラウザで動く関数だと思って読んできたはずです。

半分は違います。いまの React には、サーバーで動くコンポーネントがあります。

この Part では「データをどこに置くか」を扱いますが、 その前にこの土台をはっきりさせておきます。ここを飛ばすと、このあとの章がぼやけます。

境目は、たった 1 行

"use client";

ファイルのいちばん上にこの 1 行があるかどうか。それだけで、どこで動くかが決まります

Server ComponentClient Component
書き方何も書かない(こちらが既定)"use client"
動く場所サーバーブラウザ
実行の回数1 回だけstate が変わるたび何度でも
useState / onClick使えない使える
コードが届く先ブラウザに送られないブラウザに送られる

既定はサーバー側です。何も書かなければサーバーで動きます。"use client"「ここから先はブラウザにも渡してください」という申告です。

client-part.tsx

// この 1 行が境目。ここから下はブラウザにも届く
"use client";

import { RenderBox } from "@/components/lesson/render-box";
import { Button } from "@/components/ui/button";
import { type ReactNode, useState } from "react";

export function ClientPart({ children }: { children: ReactNode }) {
  const [count, setCount] = useState(0);

  return (
    <div className="flex flex-col gap-3">
      <RenderBox title="ブラウザ側(Client Component)" tone="highlight">
        <Button size="sm" onClick={() => setCount((current) => current + 1)}>
          押した回数: {count}
        </Button>
      </RenderBox>

      {/* サーバーで作られたものを、そのまま置ける */}
      {children}
    </div>
  );
}

並べて見る

外側がブラウザ側、中に入っている枠がサーバー側です。

サーバーで作ったものを、ブラウザ側の部品に置く

押すと上の枠だけが光る。下の枠は光らない

ブラウザ側(Client Component)

サーバー側(Server Component)数えるものがない

ここはサーバーで作られました。

この文字列はサーバー側にあります

ボタンを押すと、上の枠だけが光ります。 下の点線の枠は、何度押しても光りません。数字すら出ていません。

サーバー側の部品は、もう終わっているからです。すでに出来上がった結果が置いてあるだけで、 実行されるコードはそこにありません。

server-part.tsx

/*
  "use client" が付いていない = Server Component。
  この関数はサーバーで 1 回だけ実行され、
  結果の HTML だけがブラウザに届く。
*/

// サーバーでしか読めないもの(例)。ブラウザには渡らない
const SECRET_NOTE = "この文字列はサーバー側にあります";

export function ServerPart() {
  return (
    <div className="rounded-md border border-dashed p-3 text-sm">
      <p className="mb-1.5 font-semibold">
        サーバー側(Server Component)
        <span className="ml-2 font-normal text-muted-foreground">
          数えるものがない
        </span>
      </p>
      <p>ここはサーバーで作られました。</p>
      <p className="text-muted-foreground">{SECRET_NOTE}</p>

      {/* useState も onClick も、ここでは使えない */}
    </div>
  );
}

組み合わせ方の決まり

ここが引っかかりやすいところです。

// ✕ ブラウザ側の中で、サーバー側の部品を import する
"use client";
import { ServerPart } from "./server-part";   // エラーは出ない。だが巻き込まれる

"use client" の中で読み込んだものは、すべてブラウザ側になってしまいます。 境目はファイル単位だからです。

では、さっきのデモはどうやっていたのか。

// ◯ 外側(サーバー側)で組み立てて、children として渡す
<ClientPart>
  <ServerPart />
</ClientPart>

Part 2 でやった children です。ClientPart は中身が何かを知りません。 渡された出来上がったものを置くだけです。

client-part.tsx

// この 1 行が境目。ここから下はブラウザにも届く
"use client";

import { RenderBox } from "@/components/lesson/render-box";
import { Button } from "@/components/ui/button";
import { type ReactNode, useState } from "react";

export function ClientPart({ children }: { children: ReactNode }) {
  const [count, setCount] = useState(0);

  return (
    <div className="flex flex-col gap-3">
      <RenderBox title="ブラウザ側(Client Component)" tone="highlight">
        <Button size="sm" onClick={() => setCount((current) => current + 1)}>
          押した回数: {count}
        </Button>
      </RenderBox>

      {/* サーバーで作られたものを、そのまま置ける */}
      {children}
    </div>
  );
}

この教材も、そうなっています

いま読んでいるこのページ自体が Server Component です。本文も見出しも、サーバーで作られています。

"use client" が付いているのは、操作が要るところだけです。

  • デモ(押すと動くもの)
  • クイズ(選ぶと答え合わせをするもの)
  • 右のコードペイン(スクロールに追従するもの)

右ペインに出ているコードも、サーバー側でファイルを読んで色を付けたものです。 色付けの処理はブラウザに送られていません。

server-part.tsx

/*
  "use client" が付いていない = Server Component。
  この関数はサーバーで 1 回だけ実行され、
  結果の HTML だけがブラウザに届く。
*/

// サーバーでしか読めないもの(例)。ブラウザには渡らない
const SECRET_NOTE = "この文字列はサーバー側にあります";

export function ServerPart() {
  return (
    <div className="rounded-md border border-dashed p-3 text-sm">
      <p className="mb-1.5 font-semibold">
        サーバー側(Server Component)
        <span className="ml-2 font-normal text-muted-foreground">
          数えるものがない
        </span>
      </p>
      <p>ここはサーバーで作られました。</p>
      <p className="text-muted-foreground">{SECRET_NOTE}</p>

      {/* useState も onClick も、ここでは使えない */}
    </div>
  );
}

理解できたか確かめる

確認クイズ

何も書かないコンポーネントは、どこで動く?

確認クイズ

サーバー側の部品で useState が使えないのはなぜ?

確認クイズ

ブラウザ側の部品の中に、サーバー側の部品を置きたい。どうする?

client-part.tsx

// この 1 行が境目。ここから下はブラウザにも届く
"use client";

import { RenderBox } from "@/components/lesson/render-box";
import { Button } from "@/components/ui/button";
import { type ReactNode, useState } from "react";

export function ClientPart({ children }: { children: ReactNode }) {
  const [count, setCount] = useState(0);

  return (
    <div className="flex flex-col gap-3">
      <RenderBox title="ブラウザ側(Client Component)" tone="highlight">
        <Button size="sm" onClick={() => setCount((current) => current + 1)}>
          押した回数: {count}
        </Button>
      </RenderBox>

      {/* サーバーで作られたものを、そのまま置ける */}
      {children}
    </div>
  );
}

この章のまとめ

  • 境目は "use client" の 1 行。既定はサーバー側
  • サーバー側は1 回だけ実行され、結果だけが届く。 そのコードはブラウザに送られない
  • だから useStateonClick も使えない
  • 組み合わせるときはimport せず、children として渡す
  • この教材のページ自体も、ほとんどがサーバー側

client-part.tsx

// この 1 行が境目。ここから下はブラウザにも届く
"use client";

import { RenderBox } from "@/components/lesson/render-box";
import { Button } from "@/components/ui/button";
import { type ReactNode, useState } from "react";

export function ClientPart({ children }: { children: ReactNode }) {
  const [count, setCount] = useState(0);

  return (
    <div className="flex flex-col gap-3">
      <RenderBox title="ブラウザ側(Client Component)" tone="highlight">
        <Button size="sm" onClick={() => setCount((current) => current + 1)}>
          押した回数: {count}
        </Button>
      </RenderBox>

      {/* サーバーで作られたものを、そのまま置ける */}
      {children}
    </div>
  );
}