サーバーとクライアントのコンポジションパターン
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
パッケージを使用すると、開発者が誤ってこれらのモジュールをクライアントコンポーネントにインポートした場合にビルド時エラーを発生させることができます。
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"
ディレクティブがあるため、クライアントコンポーネント内では期待どおりに動作しますが、サーバーコンポーネント内では動作しません。
例えば、仮説上のacme-carousel
パッケージをインストールしており、<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の状態とコンテキストに依存し、通常アプリケーションのルートで必要とされるため、例外です。サードパーティのコンテキストプロバイダーについては以下で詳しく学びましょう。
コンテキストプロバイダーの使用
コンテキストプロバイダーは通常、現在のテーマのようなグローバルな懸念を共有するために、アプリケーションのルート近くでレンダリングされます。Reactコンテキストはサーバーコンポーネントでサポートされていないため、アプリケーションのルートでコンテキストを作成しようとするとエラーが発生します。
import { createContext } from 'react'
// createContext is not supported in Server Components
export const ThemeContext = createContext({})
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
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"
ディレクティブを削除する可能性があることに注意してください。React Wrap BalancerとVercel Analyticsのリポジトリで、esbuildが"use client"
ディレクティブを含むように設定する方法の例を見つけることができます。
クライアントコンポーネント
クライアントコンポーネントをツリーの下位へ移動する
クライアントの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
プロップを使用して、クライアントコンポーネントに「スロット」を作成することです。
以下の例では、<ClientComponent>
はchildren
プロップを受け入れます。
'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>
がクライアントでレンダリングされるずっと前に、サーバーでレンダリングできます。
知っておくと良いこと
- 「コンテンツを持ち上げる(lifting content up)」パターンは、親コンポーネントが再レンダリングされたときに、ネストされた子コンポーネントが再レンダリングされるのを避けるために使用されてきました。
children
プロップに限定されません。任意のプロップを使用してJSXを渡すことができます。
このページは役に立ちましたか?