Client Component の中に Server Component を置く
Client Component の中に Server Component を置きたいときは、import ではなく children で渡します。
#import と children は別のこと
「Client Component の中で Server Component を使いたい」と思ったとき、書き方は2つあります。片方は動き、片方はビルドで落ちます。
// ❌ Modal.tsx の中で import する
'use client'
import { Cart } from './Cart' // Cart がクライアント側の依存になる
export function Modal() {
return <div><Cart /></div>
}
// ✅ Server Component 側で作って、children で渡す
export default function Page() {
return (
<Modal>
<Cart />
</Modal>
)
}
違いは「Cart のコードが、どのファイルから import されているか」だけです。
- 上は Client Component のファイルから import している → Cart もクライアント側の依存になる
- 下は Server Component のファイルから import している → Cart はサーバー側に残る
置かれる位置ではなく、import した位置で決まります。
#渡っているのは、描いたあとの結果
下の書き方で Modal が受け取るのは、Cart というコンポーネントではありません。サーバーですでに描き終えた結果です。
Next.js の公式ドキュメントも、この形では Server Component が先にサーバーで描画され、RSC ペイロードにはその結果と、Client Component を置く位置の目印が入ると説明しています。
つまり Modal から見ると、children は「もう出来上がっている中身」です。中で何が起きたかは知りません。データベースを読んでいても、API キーを使っていても、Modal には関係ありません。
この性質のおかげで、
- 開閉やタブ切り替えの状態 → ブラウザ
- 中身のデータ取得 → サーバー
を、同じ画面の中で分けられます。
#エラーは、引き込んだ側ではなく引き込まれた側に出る
うっかり import してしまったときのエラーは、少し読みにくい形で出ます。
./components/Cart.tsx:1:1
Error: You're importing a module that depends on "next/headers".
This API is only available in Server Components in the App Router,
but you are using it in the Pages Router.
指されているのは Cart.tsx の1行目です。しかし直すべきなのは Modal.tsx の import です。Cart は悪くありません。
さらに、末尾に「but you are using it in the Pages Router」と出ます。App Router で書いていてもこの文が出ます。Pages Router を使っているわけではないので、ここは無視してください。
犯人を見つけるには、エラーの下に出る Import traces を下から上へ読みます。
Import traces:
Client Component Browser:
./components/Cart.tsx ← 巻き込まれた
./components/Modal.tsx ← ここに use client がある
./app/page.tsx
use client があるファイルが、境界の入口です。
#Provider も同じ形
React の Context は Server Component では使えません。だからテーマや認証状態を配るには、Provider を Client Component にする必要があります。
// app/theme-provider.tsx
'use client'
export function ThemeProvider({ children }: { children: React.ReactNode }) {
return <ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>
}
// app/layout.tsx ← Server Component のまま
export default function RootLayout({ children }) {
return (
<html lang="ja">
<body>
<ThemeProvider>{children}</ThemeProvider>
</body>
</html>
)
}
Provider は Client Component ですが、その中の children は Server Component のままです。layout も Server Component のままで構いません。
公式ドキュメントは、Provider を html や body ではなく children のできるだけ近くに置くことを勧めています。包む範囲が広いほど、静的に扱える部分が減るからです。
#外部のライブラリを使うとき
useState を使っているのに 'use client' が書かれていないパッケージがあります。そのまま Server Component から使うとエラーになります。
このときも解決の形は同じで、自分のファイルで包みます。
// components/Carousel.tsx
'use client'
export { Carousel } from 'acme-carousel'
これで、そのパッケージは自分の管理下にある境界の内側に入ります。
#まとめ
| やりたいこと | 書き方 |
|---|---|
| Client の中に Server を置く | children や props で渡す |
| Context を配る | Provider だけを Client にして children を包む |
| use client の無い外部部品を使う | 自分のファイルで再 export して包む |
| Client の中で import する | できない。ビルドで落ちる |
判断に迷ったら、Server と Client の選び方も合わせて読んでください。境界を越える props の制限はServer から Client へ渡せる値にまとめてあります。