本文へ移動
verified onnext@16.3.5react@19
最終検証日 2026-09-26
Next.js道場

Client Component の中に Server Component を置く

Client Component の中に Server Component を置きたいときは、import ではなく children で渡します。

import と children は別のこと

「Client Component の中で Server Component を使いたい」と思ったとき、書き方は2つあります。片方は動き、片方はビルドで落ちます。

同じ「Modal の中に Cart」でも、書き方で結果が変わる
うまくいかない
// Modal.tsx
'use client'
 
import { Cart } from './Cart'
 
export function Modal() {
  return <div><Cart /></div>
}

Client Component のファイルから import しているので、Cart もクライアント側の依存になる。

こう書く
// page.tsx(Server Component)
export default function Page() {
  return (
    <Modal>
      <Cart />
    </Modal>
  )
}

Server Component のファイルから import しているので、Cart はサーバー側に残る。

違いは「Cart のコードが、どのファイルから import されているか」だけです。

  • 上は Client Component のファイルから import している → Cart もクライアント側の依存になる
  • 下は Server Component のファイルから import している → Cart はサーバー側に残る

置かれる位置ではなく、import した位置で決まります。

children で渡したときの境界。Cart は境界の外側に残る
サーバー側に残るもの
page.tsx
Cart.tsx
cookies() / db
ブラウザにも送られるもの
Modal.tsx
useState(開閉)

越えられる: Cart を描き終えた結果(children)

越えられない: Cart.tsx そのもの(コードは渡らない)

渡っているのは、描いたあとの結果

下の書き方で Modal が受け取るのは、Cart というコンポーネントではありません。サーバーですでに描き終えた結果です。

Next.js の公式ドキュメントも、この形では Server Component が先にサーバーで描画され、RSC ペイロードにはその結果と、Client Component を置く位置の目印が入ると説明しています。

つまり Modal から見ると、children は「もう出来上がっている中身」です。中で何が起きたかは知りません。データベースを読んでいても、API キーを使っていても、Modal には関係ありません。

Modal の側から見ると、中身は「結果」でしかない
ブラウザに届く
Cart を描き終えた結果(HTML と RSC ペイロード)
Modal 自身の JavaScript(開閉の状態を持つため)
サーバーに残る
Cart.tsx のコード
Cart が読んだ Cookie や DB の中身
children で渡したとき、何がどの順で起きるか
  1. Server
    Page が Cart を描く

    cookies() も db もサーバーで動く。ここで中身が確定する。

  2. Server
    描き終えた結果を Modal に渡す

    Modal が受け取るのはコンポーネントではなく、出来上がった中身。

  3. Browser
    Modal だけがブラウザで動く

    開閉の状態を持つのは 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 にする必要があります(Server Component のまま書いたときのエラーはcreateContext を Server 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 へ渡せる値にまとめてあります。

出典

このページの内容は上記の一次情報を基準に書いています。個人ブログや二次情報を根拠にしていません。

理解できたか試す

では、このコードはどうなりますか。

Modal は Client Component です。その children に置いた Cart は、どこで実行されますか。

この問題を解くこのテーマの問題 3 問