サーバーおよびクライアントのコンポジションパターン
Reactアプリケーションを構築する際には、アプリケーションのどの部分をサーバーまたはクライアントでレンダリングするかを検討する必要があります。このページでは、サーバーコンポーネントとクライアントコンポーネントを使用する場合の推奨コンポジションパターンについて説明します。
サーバーコンポーネントとクライアントコンポーネントをいつ使用するか?
サーバーコンポーネントとクライアントコンポーネントのさまざまなユースケースの簡単な概要を以下に示します
何をする必要がありますか? | サーバーコンポーネント | クライアントコンポーネント |
---|---|---|
データをフェッチする | ||
バックエンドリソースにアクセスする(直接) | ||
機密情報をサーバーに保持する(アクセストークン、APIキーなど) | ||
大きな依存関係をサーバーに保持する/クライアント側のJavaScriptを削減する | ||
インタラクティビティとイベントリスナーを追加する(onClick() 、onChange() など) | ||
状態とライフサイクルエフェクトを使用する(useState() 、useReducer() 、useEffect() など) | ||
ブラウザのみのAPIを使用する | ||
状態、エフェクト、またはブラウザのみのAPIに依存するカスタムフックを使用する | ||
Reactクラスコンポーネントを使用する |
サーバーコンポーネントパターン
クライアント側のレンダリングを選択する前に、データのフェッチ、データベースやバックエンドサービスへのアクセスなど、サーバー上でいくつか作業を行いたい場合があります。
サーバーコンポーネントを操作する際の一般的なパターンを以下に示します
コンポーネント間でデータを共有する
サーバーでデータをフェッチする場合、異なるコンポーネント間でデータを共有する必要がある場合があります。たとえば、レイアウトとページが同じデータに依存している場合があります。
サーバー上で利用できないReact Contextを使用したり、propsとしてデータを渡したりする代わりに、必要なコンポーネントでfetch
またはReactのcache
関数を使用して同じデータを取得できます。同じデータに対する重複したリクエストを心配する必要はありません。これは、Reactがfetch
を拡張してデータリクエストを自動的にメモ化するためであり、fetch
が利用できない場合はcache
関数を使用できるためです。
このパターンの例を見る。
サーバー専用のコードをクライアント環境から分離する
JavaScriptモジュールはサーバーとクライアントの両方のコンポーネントモジュール間で共有できるため、サーバーでのみ実行されることを意図していたコードがクライアントに紛れ込む可能性があります。
たとえば、次のデータ取得関数を考えてみましょう。
export async function getData() {
const res = await fetch('https://external-service.com/data', {
headers: {
authorization: process.env.API_KEY,
},
})
return res.json()
}
一見すると、getData
はサーバーとクライアントの両方で動作するようです。ただし、この関数には、サーバーでのみ実行されることを意図して記述されたAPI_KEY
が含まれています。
環境変数API_KEY
にはNEXT_PUBLIC
プレフィックスが付いていないため、サーバーでのみアクセスできるプライベート変数です。環境変数がクライアントに漏洩するのを防ぐために、Next.jsはプライベート環境変数を空の文字列に置き換えます。
その結果、getData()
はクライアントでインポートして実行できますが、期待どおりには機能しません。また、変数を公開することで関数がクライアントで動作するようになりますが、機密情報をクライアントに公開したくない場合があります。
このようなサーバーコードの意図しないクライアント使用を防ぐために、server-only
パッケージを使用して、誤ってこれらのモジュールの1つをクライアントコンポーネントにインポートした場合、他の開発者にビルド時のエラーを表示できます。
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()
}
これで、getData()
をインポートするクライアントコンポーネントは、このモジュールがサーバーでのみ使用できることを説明するビルド時のエラーを受け取るようになります。
対応するパッケージclient-only
を使用して、クライアント専用のコード(たとえば、window
オブジェクトにアクセスするコード)を含むモジュールをマークできます。
サードパーティのパッケージとプロバイダーの使用
サーバーコンポーネントは新しいReact機能であるため、エコシステム内のサードパーティのパッケージとプロバイダーは、useState
、useEffect
、createContext
などのクライアント専用機能を使用するコンポーネントに"use client"
ディレクティブを追加し始めたばかりです。
現在、クライアント専用機能を使用するnpm
パッケージの多くのコンポーネントには、まだディレクティブがありません。これらのサードパーティコンポーネントは、"use client"
ディレクティブがあるため、クライアントコンポーネント内では期待どおりに動作しますが、サーバーコンポーネント内では動作しません。
たとえば、<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>
{/* Works, since Carousel is used within a Client Component */}
{isOpen && <Carousel />}
</div>
)
}
ただし、サーバーコンポーネント内で直接使用しようとすると、エラーが表示されます。
import { Carousel } from 'acme-carousel'
export default function Page() {
return (
<div>
<p>View pictures</p>
{/* Error: `useState` can not be used within Server Components */}
<Carousel />
</div>
)
}
これは、Next.jsが<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>
{/* Works, since Carousel is a Client Component */}
<Carousel />
</div>
)
}
クライアントコンポーネント内で使用することが多いため、ほとんどのサードパーティのコンポーネントをラップする必要はないと考えられます。ただし、プロバイダーは、Reactの状態とコンテキストに依存しており、通常はアプリケーションのルートで必要になるため、1つの例外です。サードパーティのコンテキストプロバイダーの詳細については以下を参照してください。
コンテキストプロバイダーの使用
コンテキストプロバイダーは、現在のテーマなどのグローバルな問題を共有するために、通常はアプリケーションのルート近くにレンダリングされます。Reactコンテキストはサーバーコンポーネントではサポートされていないため、アプリケーションのルートでコンテキストを作成しようとするとエラーが発生します。
import { createContext } from 'react'
// createContext is not supported in Server Components
export const ThemeContext = createContext({})
export default function RootLayout({ children }) {
return (
<html>
<body>
<ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>
</body>
</html>
)
}
これを修正するには、コンテキストを作成し、クライアントコンポーネント内でそのプロバイダーをレンダリングします。
'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>
}
プロバイダーはクライアントコンポーネントとしてマークされているため、サーバーコンポーネントはプロバイダーを直接レンダリングできるようになります。
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がサーバーコンポーネントの静的な部分を最適化しやすくなります。
ライブラリ作成者へのアドバイス
同様に、他の開発者が使用するパッケージを作成するライブラリ作成者は、"use client"
ディレクティブを使用して、パッケージのクライアントエントリーポイントをマークできます。これにより、パッケージのユーザーは、ラッピング境界を作成することなく、パッケージコンポーネントをサーバーコンポーネントに直接インポートできます。
ツリーのより深い位置で「use client」をより深く使用することで、パッケージを最適化して、インポートされたモジュールをサーバーコンポーネントモジュールグラフの一部にすることができます。
一部のバンドラーは"use client"
ディレクティブを削除する可能性があることに注意してください。"use client"
ディレクティブをインクルードするようにesbuildを構成する方法の例は、React Wrap BalancerおよびVercel Analyticsリポジトリにあります。
クライアントコンポーネント
ツリーの下にクライアントコンポーネントを移動する
クライアントJavaScriptバンドルのサイズを小さくするために、クライアントコンポーネントをコンポーネントツリーの下に移動することをお勧めします。
たとえば、静的要素(ロゴ、リンクなど)と状態を使用するインタラクティブな検索バーを持つレイアウトがある場合があります。
レイアウト全体をクライアントコンポーネントにする代わりに、インタラクティブなロジックをクライアントコンポーネント(<SearchBar />
など)に移動し、レイアウトをサーバーコンポーネントとして維持します。つまり、レイアウトのコンポーネントJavaScriptをすべてクライアントに送信する必要はありません。
// SearchBar is a Client Component
import SearchBar from './searchbar'
// Logo is a Server Component
import Logo from './logo'
// Layout is a Server Component by default
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<>
<nav>
<Logo />
<SearchBar />
</nav>
<main>{children}</main>
</>
)
}
サーバーからクライアントコンポーネントへのpropsの渡し(シリアル化)
サーバーコンポーネントでデータを取得した場合、そのデータを props としてクライアントコンポーネントに渡したい場合があります。サーバーからクライアントコンポーネントに渡される props は、React によってシリアライズ可能である必要があります。
クライアントコンポーネントがシリアライズ不可能なデータに依存している場合は、サードパーティライブラリを使用してクライアント側でデータを取得するか、ルートハンドラーを使用してサーバー側でデータを取得できます。
サーバーコンポーネントとクライアントコンポーネントのインターリーブ
クライアントコンポーネントとサーバーコンポーネントをインターリーブする場合、UI をコンポーネントのツリーとして視覚化すると役立つ場合があります。ルートレイアウト(サーバーコンポーネント)から始め、"use client"
ディレクティブを追加することで、特定のコンポーネントのサブツリーをクライアント側でレンダリングできます。
これらのクライアントサブツリー内では、引き続きサーバーコンポーネントをネストしたり、サーバーアクションを呼び出したりできますが、留意すべき点がいくつかあります。
- リクエスト-レスポンスライフサイクル中、コードはサーバーからクライアントに移動します。クライアント側でサーバー上のデータまたはリソースにアクセスする必要がある場合は、サーバーへの新しいリクエストを行うことになり、行き来を切り替えるわけではありません。
- サーバーへの新しいリクエストが行われると、クライアントコンポーネント内にネストされているものを含め、すべてのサーバーコンポーネントが最初にレンダリングされます。レンダリングされた結果(RSCペイロード)には、クライアントコンポーネントの場所への参照が含まれます。その後、クライアント側では、ReactはRSCペイロードを使用して、サーバーコンポーネントとクライアントコンポーネントを単一のツリーに調整します。
- クライアントコンポーネントはサーバーコンポーネントの後にレンダリングされるため、クライアントコンポーネントモジュールにサーバーコンポーネントをインポートすることはできません(サーバーへの新しいリクエストが必要になるため)。代わりに、サーバーコンポーネントをクライアントコンポーネントに
props
として渡すことができます。以下のサポートされていないパターンとサポートされているパターンのセクションを参照してください。
サポートされていないパターン: サーバーコンポーネントをクライアントコンポーネントにインポートする
次のパターンはサポートされていません。クライアントコンポーネントにサーバーコンポーネントをインポートすることはできません。
'use client'
// You cannot import a Server Component into a Client Component.
import ServerComponent from './Server-Component'
export default function ClientComponent({
children,
}: {
children: React.ReactNode
}) {
const [count, setCount] = useState(0)
return (
<>
<button onClick={() => setCount(count + 1)}>{count}</button>
<ServerComponent />
</>
)
}
サポートされているパターン: サーバーコンポーネントを props としてクライアントコンポーネントに渡す
次のパターンはサポートされています。サーバーコンポーネントを props としてクライアントコンポーネントに渡すことができます。
一般的なパターンは、React の children
prop を使用して、クライアントコンポーネントに"スロット"を作成することです。
以下の例では、<ClientComponent>
は children
prop を受け入れます。
'use client'
import { useState } from 'react'
export default function ClientComponent({
children,
}: {
children: React.ReactNode
}) {
const [count, setCount] = useState(0)
return (
<>
<button onClick={() => setCount(count + 1)}>{count}</button>
{children}
</>
)
}
<ClientComponent>
は、最終的に children
がサーバーコンポーネントの結果で埋められることを知りません。<ClientComponent>
が持つ唯一の責任は、最終的に children
を配置する場所を決定することです。
親サーバーコンポーネントでは、<ClientComponent>
と <ServerComponent>
の両方をインポートし、<ServerComponent>
を <ClientComponent>
の子として渡すことができます。
// This pattern works:
// You can pass a Server Component as a child or prop of a
// Client Component.
import ClientComponent from './client-component'
import ServerComponent from './server-component'
// Pages in Next.js are Server Components by default
export default function Page() {
return (
<ClientComponent>
<ServerComponent />
</ClientComponent>
)
}
このアプローチにより、<ClientComponent>
と <ServerComponent>
は分離され、独立してレンダリングできます。この場合、子である <ServerComponent>
は、<ClientComponent>
がクライアントでレンダリングされるよりもずっと前に、サーバーでレンダリングできます。
知っておくと良いこと
- "コンテンツを上に持ち上げる"パターンは、親コンポーネントが再レンダリングされたときにネストされた子コンポーネントの再レンダリングを回避するために使用されてきました。
children
prop に限定されません。JSX を渡すために任意の prop を使用できます。
この記事は役に立ちましたか?