コンテンツにスキップ
App Routerはじめにサーバーコンポーネントとクライアントコンポーネント

サーバーコンポーネントとクライアントコンポーネント

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

このページでは、Next.js でサーバーコンポーネントとクライアントコンポーネントがどのように機能するか、およびそれらをいつ使用するかについて、アプリケーションでそれらを組み合わせて使用する方法の例とともに説明します。

サーバーコンポーネントとクライアントコンポーネントの使い分け

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

クライアントコンポーネントを使用するのは、次のような場合です。

サーバーコンポーネントを使用するのは、次のような場合です。

  • データベースや API からソースに近い場所でデータを取得する。
  • API キー、トークン、その他のシークレットをクライアントに公開せずに使用する。
  • ブラウザに送信される JavaScript の量を削減する。
  • 初回コンテンツ描画(FCP)を改善し、コンテンツをクライアントに段階的にストリーミングする。

たとえば、<Page> コンポーネントは、投稿に関するデータを取得し、それをクライアントサイドのインタラクティビティを処理する <LikeButton> に props として渡すサーバーコンポーネントです。

app/[id]/page.tsx
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
'use client'
 
import { useState } from 'react'
 
export default function LikeButton({ likes }: { likes: number }) {
  // ...
}

Next.js では、サーバーコンポーネントとクライアントコンポーネントはどのように機能しますか?

サーバー側

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

  • サーバーコンポーネントは、React Server Component Payload (RSC Payload) と呼ばれる特別なデータ形式にレンダリングされます。
  • クライアントコンポーネントと RSC Payload は、HTML をプリレンダリングするために使用されます。

React Server Component Payload (RSC) とは?

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

  • サーバーコンポーネントのレンダリング結果
  • クライアントコンポーネントをレンダリングする場所のプレースホルダーとその JavaScript ファイルへの参照
  • サーバーコンポーネントからクライアントコンポーネントに渡されたすべての props

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

次に、クライアント側で

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

ハイドレーションとは?

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

後続のナビゲーション

後続のナビゲーションで

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

クライアントコンポーネントの使用

クライアントコンポーネントを作成するには、インポートの上にファイルの上部に"use client"ディレクティブを追加します。

app/ui/counter.tsx
'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" は、サーバーとクライアントのモジュールグラフ(ツリー)の間の境界を宣言するために使用されます。

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

JavaScript バンドルサイズの削減

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

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

app/layout.tsx
// Client Component
import Search from './search'
// Server Component
import Logo from './logo'
 
// Layout is a Server Component by default
export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    <>
      <nav>
        <Logo />
        <Search />
      </nav>
      <main>{children}</main>
    </>
  )
}
app/ui/search.tsx
'use client'
 
export default function Search() {
  // ...
}

サーバーコンポーネントからクライアントコンポーネントへのデータ渡し

サーバーコンポーネントからクライアントコンポーネントにデータを渡すには、props を使用します。

app/[id]/page.tsx
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
'use client'
 
export default function LikeButton({ likes }: { likes: number }) {
  // ...
}

または、use Hook を使用して、サーバーコンポーネントからクライアントコンポーネントにデータをストリーミングできます。例については、を参照してください。

知っておくと良いこと: クライアントコンポーネントに渡される props は、React によってシリアライズ可能 である必要があります。

サーバーコンポーネントとクライアントコンポーネントのインターリーブ

サーバーコンポーネントをクライアントコンポーネントの prop として渡すことができます。これにより、クライアントコンポーネント内にサーバーレンダリングされた UI を視覚的にネストさせることができます。

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

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

次に、親のサーバーコンポーネント(例:<Page>)で、<Modal> の子として <Cart> を渡すことができます。

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

このパターンでは、すべてのサーバーコンポーネントは事前にサーバーでレンダリングされます。これには props として渡されるものも含まれます。結果の RSC Payload には、コンポーネントツリー内のどこにクライアントコンポーネントをレンダリングするかを示す参照が含まれます。

コンテキストプロバイダー

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

コンテキストを使用するには、children を受け入れるクライアントコンポーネントを作成します。

app/theme-provider.tsx
'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>
}

次に、それをサーバーコンポーネント(例:layout)にインポートします。

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

これにより、サーバーコンポーネントからプロバイダーを直接レンダリングできるようになり、アプリ全体の他のすべてのクライアントコンポーネントがこのコンテキストを利用できるようになります。

知っておくと良いこと: プロバイダーはツリーの可能な限り深くレンダリングする必要があります。ThemeProvider が HTML ドキュメント全体ではなく {children} のみをラップしていることに注意してください。これにより、Next.js がサーバーコンポーネントの静的部分を最適化しやすくなります。

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

クライアント専用機能に依存するサードパーティコンポーネントを使用する場合、クライアントコンポーネントでラップして、期待どおりに機能することを確認できます。

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

クライアントコンポーネント内で <Carousel /> を使用すると、期待どおりに機能します。

app/gallery.tsx
'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>
      {/* Works, since Carousel is used within a Client Component */}
      {isOpen && <Carousel />}
    </div>
  )
}

しかし、サーバーコンポーネントで直接使用しようとすると、エラーが発生します。これは、Next.js が <Carousel /> がクライアント専用機能を使用していることを認識していないためです。

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

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

これで、サーバーコンポーネントで <Carousel /> を直接使用できます。

app/page.tsx
import Carousel from './carousel'
 
export default function Page() {
  return (
    <div>
      <p>View pictures</p>
      {/*  Works, since Carousel is a Client Component */}
      <Carousel />
    </div>
  )
}

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

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

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

環境汚染の防止

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

lib/data.ts
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() はクライアントでインポートして実行できますが、期待どおりには機能しません。

クライアントコンポーネントでの誤用を防ぐために、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-only パッケージ は、window オブジェクトにアクセスするコードのような、クライアント専用ロジックを含むモジュールをマークするために使用できます。

Next.js では、server-only または client-only のインストールはオプションです。ただし、リンティングルールで余分な依存関係がフラグ付けされた場合は、問題を回避するためにそれらをインストールできます。

ターミナル
pnpm add server-only

Next.js は、モジュールが誤った環境で使用された場合に、より明確なエラーメッセージを提供するために server-only および client-only を内部的に処理します。これらのパッケージの NPM からの内容は、Next.js によって使用されません。

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