Part 9 · state を共有する
47 / 64 章
サーバーで動くコンポーネント
"use client" の 1 行が何を分けているのか
ここまでずっと、コンポーネント=ブラウザで動く関数だと思って読んできたはずです。
半分は違います。いまの React には、サーバーで動くコンポーネントがあります。
この Part では「データをどこに置くか」を扱いますが、 その前にこの土台をはっきりさせておきます。ここを飛ばすと、このあとの章がぼやけます。
境目は、たった 1 行
"use client";ファイルのいちばん上にこの 1 行があるかどうか。それだけで、どこで動くかが決まります。
| Server Component | Client 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>
);
}並べて見る
外側がブラウザ側、中に入っている枠がサーバー側です。
押すと上の枠だけが光る。下の枠は光らない
サーバー側(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 回だけ実行され、結果だけが届く。 そのコードはブラウザに送られない
- だから
useStateもonClickも使えない - 組み合わせるときは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>
);
}