Menu

Server コンポーネントと Client コンポーネント

デフォルトでは、レイアウトとページは Server コンポーネント であり、データをフェッチしてサーバー上で UI の一部をレンダリングし、結果をオプションでキャッシュしてクライアントにストリーミングできます。インタラクティブ機能またはブラウザ API が必要な場合は、Client コンポーネント を使用して機能を追加できます。

このページでは、Server コンポーネントと Client コンポーネントが Next.js でどのように機能するか、どのような場合に使用するか、およびアプリケーション内でそれらを一緒に構成する方法の例について説明します。

Server コンポーネントと Client コンポーネントはいつ使用すればよいですか?

クライアント環境とサーバー環境には異なる機能があります。Server コンポーネントと Client コンポーネントを使用すると、ユースケースに応じて各環境でロジックを実行できます。

以下の場合、Client コンポーネントを使用します:

以下の場合、Server コンポーネントを使用します:

  • データベースまたは API からソースの近くでデータをフェッチします。
  • API キー、トークン、およびその他のシークレットをクライアントに公開せずに使用します。
  • ブラウザに送信される JavaScript の量を削減します。
  • First Contentful Paint(FCP) を改善し、コンテンツをクライアントに段階的にストリーミングします。

例えば、<Page> コンポーネントは Server コンポーネントでありポストに関するデータをフェッチし、それを props として <LikeButton> に渡しており、<LikeButton> はクライアント側のインタラクティブ機能を処理します。

app/[id]/page.tsx
TypeScript
import LikeButton from '@/app/ui/like-button'
import { getPost } from '@/lib/data'
 
export default async function Page({
  params,
}: {
  params: Promise<{ id: string }>
}) {
  const { id } = await params
  const post = await getPost(id)
 
  return (
    <div>
      <main>
        <h1>{post.title}</h1>
        {/* ... */}
        <LikeButton likes={post.likes} />
      </main>
    </div>
  )
}
app/ui/like-button.tsx
TypeScript
'use client'
 
import { useState } from 'react'
 
export default function LikeButton({ likes }: { likes: number }) {
  // ...
}

Server コンポーネントと Client コンポーネントは Next.js でどのように機能しますか?

サーバー上

サーバー上では、Next.js は React の API を使用してレンダリングをオーケストレーションします。レンダリング作業は、個々のルートセグメント(レイアウトとページ)によってチャンクに分割されます:

  • Server コンポーネント は React Server Component Payload(RSC Payload)と呼ばれる特別なデータ形式にレンダリングされます。
  • Client コンポーネント と RSC Payload は 事前レンダリング に使用されます。

React Server Component Payload(RSC)とは何ですか?

RSC Payload は、レンダリングされた React Server コンポーネントツリーのコンパクトなバイナリ表現です。これはクライアント上の React によって使用され、ブラウザの DOM を更新します。RSC Payload には以下が含まれます:

  • Server コンポーネントのレンダリング結果
  • Client コンポーネントをレンダリングする場所のプレースホルダーと、それらの JavaScript ファイルへの参照
  • Server コンポーネントから Client コンポーネントに渡される props

クライアント上(初回ロード)

その後、クライアント上では:

  1. HTML は、ユーザーにルートの高速な非インタラクティブプレビューを即座に表示するために使用されます。
  2. RSC Payload は、Client コンポーネントと Server コンポーネントのツリーを調整するために使用されます。
  3. JavaScript は Client コンポーネントをハイドレートし、アプリケーションをインタラクティブにするために使用されます。

ハイドレーションとは何ですか?

ハイドレーションは、React が イベントハンドラー を DOM にアタッチして、静的 HTML をインタラクティブにするプロセスです。

その後のナビゲーション

その後のナビゲーションでは:

  • RSC Payload はプリフェッチされ、インスタントナビゲーション用にキャッシュされます。
  • Client コンポーネント は、サーバーでレンダリングされた HTML なしで、クライアント上で完全にレンダリングされます。

Client コンポーネントの使用

ファイルの最上部、インポートの上に "use client" ディレクティブを追加して Client コンポーネントを作成できます。

app/ui/counter.tsx
TypeScript
'use client'
 
import { useState } from 'react'
 
export default function Counter() {
  const [count, setCount] = useState(0)
 
  return (
    <div>
      <p>{count} likes</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  )
}

"use client" は、Server モジュールグラフ(ツリー)と Client モジュールグラフ(ツリー)の間に 境界 を宣言するために使用されます。

ファイルが "use client" でマークされると、そのすべてのインポートと子コンポーネントはクライアントバンドルの一部と見なされます。これは、クライアント用に設計されたすべてのコンポーネントにディレクティブを追加する必要がないことを意味します。

JS バンドルサイズの削減

クライアント JavaScript バンドルのサイズを削減するには、UI の大部分を Client コンポーネントとしてマークするのではなく、'use client' を特定のインタラクティブなコンポーネントに追加します。

例えば、<Layout> コンポーネントはロゴとナビゲーションリンクなどのほとんどの静的要素を含んでいますが、インタラクティブな検索バーが含まれています。<Search /> はインタラクティブで Client コンポーネントである必要がありますが、レイアウトの残りの部分は Server コンポーネントのままにできます。

app/layout.tsx
TypeScript
// Client コンポーネント
import Search from './search'
// Server コンポーネント
import Logo from './logo'
 
// Layout はデフォルトで Server コンポーネント
export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    <>
      <nav>
        <Logo />
        <Search />
      </nav>
      <main>{children}</main>
    </>
  )
}
app/ui/search.tsx
TypeScript
'use client'
 
export default function Search() {
  // ...
}

Server コンポーネントから Client コンポーネントへのデータの渡し

Server コンポーネントから Client コンポーネントにデータを props を使用して渡すことができます。

app/[id]/page.tsx
TypeScript
import LikeButton from '@/app/ui/like-button'
import { getPost } from '@/lib/data'
 
export default async function Page({
  params,
}: {
  params: Promise<{ id: string }>
}) {
  const { id } = await params
  const post = await getPost(id)
 
  return <LikeButton likes={post.likes} />
}
app/ui/like-button.tsx
TypeScript
'use client'
 
export default function LikeButton({ likes }: { likes: number }) {
  // ...
}

または、use フック を使用して Server コンポーネントから Client コンポーネントにデータをストリーミングできます。 を参照してください。

補足:Client コンポーネントに渡される props は React によって シリアライズ可能 である必要があります。

Server コンポーネントと Client コンポーネントのインターリーブ

Server コンポーネントを props として Client コンポーネントに渡すことができます。これにより、サーバーでレンダリングされた UI を Client コンポーネント内に視覚的にネストできます。

一般的なパターンは children を使用して <ClientComponent>スロット を作成することです。例えば、クライアント状態を使用して表示を切り替える <Modal> コンポーネント内でサーバー上でデータをフェッチする <Cart> コンポーネント。

app/ui/modal.tsx
TypeScript
'use client'
 
export default function Modal({ children }: { children: React.ReactNode }) {
  return <div>{children}</div>
}

その後、親 Server コンポーネント(例:<Page>)内で、<Cart><Modal> の子として渡すことができます:

app/page.tsx
TypeScript
import Modal from './ui/modal'
import Cart from './ui/cart'
 
export default function Page() {
  return (
    <Modal>
      <Cart />
    </Modal>
  )
}

このパターンでは、すべての Server コンポーネント(props としてのものを含む)は事前にサーバー上でレンダリングされます。結果の RSC Payload には、コンポーネントツリー内で Client コンポーネントをレンダリングする場所への参照が含まれます。

Context プロバイダー

React context は通常、現在のテーマなどのグローバル状態を共有するために使用されます。ただし、React context は Server コンポーネントではサポートされていません。

context を使用するには、children を受け入れる Client コンポーネントを作成します:

app/theme-provider.tsx
TypeScript
'use client'
 
import { createContext } from 'react'
 
export const ThemeContext = createContext({})
 
export default function ThemeProvider({
  children,
}: {
  children: React.ReactNode
}) {
  return <ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>
}

その後、Server コンポーネント(例:layout)にそれをインポートします:

app/layout.tsx
TypeScript
import ThemeProvider from './theme-provider'
 
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html>
      <body>
        <ThemeProvider>{children}</ThemeProvider>
      </body>
    </html>
  )
}

これで Server コンポーネントはプロバイダーを直接レンダリングでき、アプリケーション全体の他のすべての Client コンポーネントがこの context を使用できるようになります。

補足:プロバイダーはツリー内のできるだけ深い場所でレンダリングする必要があります。ThemeProvider がカラの <html> ドキュメント全体ではなく {children} のみをラップしていることに注目してください。これにより、Next.js は Server コンポーネントの静的部分を最適化しやすくなります。

サードパーティコンポーネント

クライアントのみの機能に依存するサードパーティコンポーネントを使用する場合、Client コンポーネントでラップして、期待通りに動作するようにすることができます。

例えば、<Carousel />acme-carousel パッケージからインポートできます。このコンポーネントは useState を使用しますが、まだ "use client" ディレクティブを持っていません。

<Carousel /> を Client コンポーネント内で使用すれば、期待通りに動作します:

app/gallery.tsx
TypeScript
'use client'
 
import { useState } from 'react'
import { Carousel } from 'acme-carousel'
 
export default function Gallery() {
  const [isOpen, setIsOpen] = useState(false)
 
  return (
    <div>
      <button onClick={() => setIsOpen(true)}>View pictures</button>
      {/* Client コンポーネント内で使用されているため、動作します */}
      {isOpen && <Carousel />}
    </div>
  )
}

ただし、Server コンポーネント内で直接使用しようとすると、エラーが表示されます。これは、Next.js が <Carousel /> がクライアントのみの機能を使用していることを知らないためです。

これを修正するには、クライアントのみの機能に依存するサードパーティコンポーネントを独自の Client コンポーネントでラップできます:

app/carousel.tsx
TypeScript
'use client'
 
import { Carousel } from 'acme-carousel'
 
export default Carousel

これで、Server コンポーネント内で <Carousel /> を直接使用できます:

app/page.tsx
TypeScript
import Carousel from './carousel'
 
export default function Page() {
  return (
    <div>
      <p>View pictures</p>
      {/* Carousel は Client コンポーネントであるため、動作します */}
      <Carousel />
    </div>
  )
}

ライブラリ作成者向けのアドバイス

コンポーネントライブラリを構築する場合、クライアントのみの機能に依存するエントリーポイントに "use client" ディレクティブを追加します。これにより、ユーザーはラッパーを作成する必要なく、Server コンポーネントにコンポーネントをインポートできます。

一部のバンドラーは "use client" ディレクティブをストリップする可能性があることに注意してください。esbuild を設定して "use client" ディレクティブを含める方法の例は、React Wrap Balancer および Vercel Analytics リポジトリで見つけることができます。

環境汚染の防止

JavaScript モジュールは Server コンポーネントと Client コンポーネントのモジュール間で共有できます。これは、誤ってサーバーのみのコードをクライアントにインポートする可能性があることを意味します。例えば、次の関数を考えます:

lib/data.ts
TypeScript
export async function getData() {
  const res = await fetch('https://external-service.com/data', {
    headers: {
      authorization: process.env.API_KEY,
    },
  })
 
  return res.json()
}

この関数には、クライアントに公開されるべきではない API_KEY が含まれています。

Next.js では、NEXT_PUBLIC_ というプレフィックスの付いた環境変数のみがクライアントバンドルに含まれます。変数にプレフィックスが付いていない場合、Next.js はそれらを空の文字列に置き換えます。

その結果、getData() をクライアント上でインポートおよび実行できても、期待通りには動作しません。

Client コンポーネントでの誤った使用を防ぐために、server-only パッケージ を使用できます。

その後、サーバーのみのコードを含むファイルにパッケージをインポートします:

lib/data.js
import 'server-only'
 
export async function getData() {
  const res = await fetch('https://external-service.com/data', {
    headers: {
      authorization: process.env.API_KEY,
    },
  })
 
  return res.json()
}

これで、モジュールを Client コンポーネントにインポートしようとするとビルド時エラーが発生します。

対応する client-only パッケージ は、window オブジェクトにアクセスするコードなど、クライアントのみのロジックを含むモジュールをマークするために使用できます。

Next.js では、server-only または client-only をインストールすることは オプション です。ただし、linting ルールが不要な依存関係にフラグを立てる場合は、問題を避けるためにそれらをインストールする場合があります。

npm install server-only
yarn add server-only
pnpm add server-only
bun add server-only

Next.js は、モジュールが間違った環境で使用される場合にわかりやすいエラーメッセージを提供するため、server-only および client-only インポートを内部的に処理します。NPM のこれらのパッケージのコンテンツは Next.js によって使用されません。

Next.js は、noUncheckedSideEffectImports がアクティブな TypeScript 設定用に、server-only および client-only 用の独自の型宣言も提供します。