サーバーコンポーネントとクライアントコンポーネントの使用方法
デフォルトでは、レイアウトとページはサーバーコンポーネントであり、サーバー上でデータを取得しUIの一部をレンダリングし、必要に応じて結果をキャッシュしてクライアントにストリーミングできます。インタラクティブ性やブラウザAPIが必要な場合は、クライアントコンポーネントを使用して機能を追加できます。
このページでは、Next.jsにおけるサーバーコンポーネントとクライアントコンポーネントの動作原理と使用タイミングを説明し、アプリケーションでこれらを組み合わせる方法の例を示します。
サーバーコンポーネントとクライアントコンポーネントの使い分け
クライアントとサーバーの環境には異なる機能があります。サーバーコンポーネントとクライアントコンポーネントを使用することで、ユースケースに応じて各環境でロジックを実行できます。
クライアントコンポーネントは以下の場合に使用します:
- 状態管理とイベントハンドラ。例:
onClick
、onChange
- ライフサイクルロジック。例:
useEffect
- ブラウザ専用API。例:
localStorage
、window
、Navigator.geolocation
など - カスタムフック
サーバーコンポーネントは以下の場合に使用します:
- データソースに近い場所(データベースやAPI)からデータを取得
- APIキー、トークン、その他の秘密情報をクライアントに公開せずに使用
- ブラウザに送信するJavaScriptの量を削減
- First Contentful Paint (FCP)を改善し、コンテンツをクライアントに段階的にストリーミング
例えば、<Page>
コンポーネントは投稿データを取得するサーバーコンポーネントで、クライアント側のインタラクティブ性を処理する<LikeButton>
にpropsとしてデータを渡します。
import LikeButton from '@/app/ui/like-button'
import { getPost } from '@/lib/data'
export default async function Page({ params }: { params: { id: string } }) {
const post = await getPost(params.id)
return (
<div>
<main>
<h1>{post.title}</h1>
{/* ... */}
<LikeButton likes={post.likes} />
</main>
</div>
)
}
import LikeButton from '@/app/ui/like-button'
import { getPost } from '@/lib/data'
export default async function Page({ params }) {
const post = await getPost(params.id)
return (
<div>
<main>
<h1>{post.title}</h1>
{/* ... */}
<LikeButton likes={post.likes} />
</main>
</div>
)
}
Next.jsにおけるサーバーとクライアントコンポーネントの動作原理
サーバー側
サーバー側では、Next.jsはReactのAPIを使用してレンダリングを調整します。レンダリング作業は個々のルートセグメント(レイアウトとページ)ごとに分割されます:
- サーバーコンポーネントはReact Server Component Payload(RSC Payload)と呼ばれる特別なデータ形式にレンダリングされます
- クライアントコンポーネントとRSC PayloadはHTMLを事前レンダリングするために使用されます
React Server Component Payload (RSC)とは?
RSC Payloadは、レンダリングされたReactサーバーコンポーネントツリーのコンパクトなバイナリ表現です。クライアント側のReactがブラウザのDOMを更新するために使用します。RSC Payloadには以下が含まれます:
- サーバーコンポーネントのレンダリング結果
- クライアントコンポーネントがレンダリングされるべき場所のプレースホルダーとそれらのJavaScriptファイルへの参照
- サーバーコンポーネントからクライアントコンポーネントに渡されるprops
クライアント側(初回ロード)
クライアント側では:
- HTMLはユーザーにルートの高速な非インタラクティブなプレビューを即座に表示するために使用されます
- RSC Payloadはクライアントとサーバーコンポーネントツリーを調整するために使用されます
- JavaScriptはクライアントコンポーネントをハイドレートし、アプリケーションをインタラクティブにするために使用されます
ハイドレーションとは?
ハイドレーションは、ReactがイベントハンドラをDOMにアタッチして静的HTMLをインタラクティブにするプロセスです。
後続のナビゲーション
後続のナビゲーションでは:
- RSC Payloadは即時ナビゲーションのために事前取得されキャッシュされます
- クライアントコンポーネントはサーバーレンダリングされたHTMLなしで、完全にクライアント側でレンダリングされます
使用例
クライアントコンポーネントの使用
ファイルの先頭(import文の上)に"use client"
ディレクティブを追加することで、クライアントコンポーネントを作成できます。
'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'
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"
がマークされると、そのすべてのインポートと子コンポーネントはクライアントバンドルの一部と見なされます。つまり、クライアント向けのすべてのコンポーネントにディレクティブを追加する必要はありません。
JSバンドルサイズの削減
クライアントJavaScriptバンドルのサイズを削減するには、UIの大部分をクライアントコンポーネントとしてマークする代わりに、特定のインタラクティブなコンポーネントに'use client'
を追加します。
例えば、<Layout>
コンポーネントにはロゴやナビゲーションリンクなどの静的な要素がほとんどですが、インタラクティブな検索バーが含まれています。<Search />
はインタラクティブでクライアントコンポーネントである必要がありますが、レイアウトの残りの部分はサーバーコンポーネントのままにできます。
'use client'
export default function Search() {
// ...
}
'use client'
export default function Search() {
// ...
}
// クライアントコンポーネント
import Search from './search'
// サーバーコンポーネント
import Logo from './logo'
// レイアウトはデフォルトでサーバーコンポーネント
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<>
<nav>
<Logo />
<Search />
</nav>
<main>{children}</main>
</>
)
}
// クライアントコンポーネント
import Search from './search'
// サーバーコンポーネント
import Logo from './logo'
// レイアウトはデフォルトでサーバーコンポーネント
export default function Layout({ children }) {
return (
<>
<nav>
<Logo />
<Search />
</nav>
<main>{children}</main>
</>
)
}
サーバーからクライアントコンポーネントへのデータ受け渡し
propsを使用して、サーバーコンポーネントからクライアントコンポーネントにデータを渡すことができます。
import LikeButton from '@/app/ui/like-button'
import { getPost } from '@/lib/data'
export default async function Page({ params }: { params: { id: string } }) {
const post = await getPost(params.id)
return <LikeButton likes={post.likes} />
}
import LikeButton from '@/app/ui/like-button'
import { getPost } from '@/lib/data'
export default async function Page({ params }) {
const post = await getPost(params.id)
return <LikeButton likes={post.likes} />
}
あるいは、サーバーコンポーネントからクライアントコンポーネントにuse
フックを使用してデータをストリーミングできます。例を参照してください。
豆知識: クライアントコンポーネントに渡されるpropsは、Reactによってシリアライズ可能である必要があります。
サーバーとクライアントコンポーネントの交互配置
サーバーコンポーネントをクライアントコンポーネントのpropとして渡すことができます。これにより、クライアントコンポーネント内にサーバーレンダリングされたUIを視覚的にネストできます。
一般的なパターンは、children
を使用して<ClientComponent>
内に_スロット_を作成することです。例えば、サーバー上でデータを取得する<Cart>
コンポーネントを、クライアント状態を使用して表示/非表示を切り替える<Modal>
コンポーネント内に配置します。
'use client'
export default function Modal({ children }: { children: React.ReactNode }) {
return <div>{children}</div>
}
'use client'
export default function Modal({ children }) {
return <div>{children}</div>
}
そして、親のサーバーコンポーネント(例: <Page>
)で、<Cart>
を<Modal>
の子として渡します:
import Modal from './ui/modal'
import Cart from './ui/cart'
export default function Page() {
return (
<Modal>
<Cart />
</Modal>
)
}
import Modal from './ui/modal'
import Cart from './ui/cart'
export default function Page() {
return (
<Modal>
<Cart />
</Modal>
)
}
このパターンでは、すべてのサーバーコンポーネントがpropsとして含まれるものも含め、事前にサーバー上でレンダリングされます。結果のRSCペイロードには、コンポーネントツリー内のどこにクライアントコンポーネントをレンダリングすべきかの参照が含まれます。
コンテキストプロバイダ
Reactコンテキストは、現在のテーマなどのグローバル状態を共有するためによく使用されます。ただし、Reactコンテキストはサーバーコンポーネントではサポートされていません。
コンテキストを使用するには、children
を受け入れるクライアントコンポーネントを作成します:
'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>
}
'use client'
import { createContext } from 'react'
export const ThemeContext = createContext({})
export default function ThemeProvider({ children }) {
return <ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>
}
次に、サーバーコンポーネント(例: layout
)にインポートします:
import ThemeProvider from './theme-provider'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html>
<body>
<ThemeProvider>{children}</ThemeProvider>
</body>
</html>
)
}
import ThemeProvider from './theme-provider'
export default function RootLayout({ children }) {
return (
<html>
<body>
<ThemeProvider>{children}</ThemeProvider>
</body>
</html>
)
}
これで、サーバーコンポーネントが直接プロバイダをレンダリングできるようになり、アプリ全体のすべてのクライアントコンポーネントがこのコンテキストを利用できるようになります。
豆知識: プロバイダはツリーのできるだけ深い場所にレンダリングする必要があります -
ThemeProvider
が<html>
ドキュメント全体ではなく{children}
のみをラップしていることに注目してください。これにより、Next.jsがサーバーコンポーネントの静的部分を最適化しやすくなります。
サードパーティコンポーネント
クライアント専用機能に依存するサードパーティコンポーネントを使用する場合、クライアントコンポーネントでラップすることで期待通りに動作させることができます。
例えば、<Carousel />
をacme-carousel
パッケージからインポートできます。このコンポーネントはuseState
を使用していますが、まだ"use client"
ディレクティブがありません。
<Carousel />
をクライアントコンポーネント内で使用すると、期待通りに動作します:
'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>
{/* クライアントコンポーネント内で使用されているため動作 */}
{isOpen && <Carousel />}
</div>
)
}
'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>
{/* クライアントコンポーネント内で使用されているため動作 */}
{isOpen && <Carousel />}
</div>
)
}
しかし、サーバーコンポーネント内で直接使用しようとすると、エラーが表示されます。これはNext.jsが<Carousel />
がクライアント専用機能を使用していることを認識しないためです。
これを修正するには、クライアント専用機能に依存するサードパーティコンポーネントを独自のクライアントコンポーネントでラップします:
'use client'
import { Carousel } from 'acme-carousel'
export default Carousel
'use client'
import { Carousel } from 'acme-carousel'
export default Carousel
これで、サーバーコンポーネント内で直接<Carousel />
を使用できます:
import Carousel from './carousel'
export default function Page() {
return (
<div>
<p>View pictures</p>
{/* Carouselがクライアントコンポーネントなので動作 */}
<Carousel />
</div>
)
}
import Carousel from './carousel'
export default function Page() {
return (
<div>
<p>View pictures</p>
{/* Carouselがクライアントコンポーネントなので動作 */}
<Carousel />
</div>
)
}
ライブラリ作者へのアドバイス
コンポーネントライブラリを構築している場合、クライアント専用機能に依存するエントリポイントに
"use client"
ディレクティブを追加してください。これにより、ユーザーがラッパーを作成することなくサーバーコンポーネントにコンポーネントをインポートできます。一部のバンドラーは
"use client"
ディレクティブを削除する可能性があることに注意してください。"use client"
ディレクティブを含めるようにesbuildを設定する方法の例は、React Wrap BalancerとVercel Analyticsリポジトリで確認できます。
環境変数の流出防止
JavaScriptモジュールはサーバーコンポーネントとクライアントコンポーネントの間で共有可能です。つまり、誤ってサーバー専用のコードをクライアント側にインポートしてしまう可能性があります。例えば、以下の関数を考えてみましょう:
export async function getData() {
const res = await fetch('https://external-service.com/data', {
headers: {
authorization: process.env.API_KEY,
},
})
return res.json()
}
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
パッケージを使用できます。
npm install server-only
そして、サーバー専用コードを含むファイルにこのパッケージをインポートします:
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
オブジェクトにアクセスするコードなど、クライアント専用ロジックを含むモジュールをマークできます。