Lang x Lang

Server and Client Composition Patterns

React アプリケーションを構築する際には、アプリケーションのどの部分を Server または client でレンダリングするべきかを検討する必要があります。このページでは、server と Client Components を使用する際の推奨されるコンポジションパターンについて説明します。

When to use Server and Client Components?

ここに Server と Client Components の異なる使用例の簡単なまとめを示します:

あなたが行う必要があることは何ですか?Server ComponentClient Component
Fetch データ
バックエンドリソースに直接アクセス
server 上に機密情報(アクセストークン、 API キーなど)を保管する
大きな依存関係は server に保持 / クライアント側の JavaScript を減らす
インタラクティビティとイベントリスナー (onClick(), onChange()など) を追加する
State とライフサイクルエフェクトを使用する (useState(), useReducer(), useEffect()など)
ブラウザ専用 API を使用する
ステート、エフェクト、またはブラウザ専用 API に依存するカスタムフックを使用する
React クラスコンポーネント の使用

Server Component Patterns

client-side rendering に参加する前に、データの取得やデータベースや backend services へのアクセスなど、server 上でいくつかの作業を行いたいかもしれません。

ここでは、Server Components を操作する際の一般的なパターンをいくつか紹介します:

Component 間でデータを共有する

server 上でデータを取得する際、異なる Component 間でデータを共有する必要がある場合があります。例えば、同じデータに依存する layout とページを持つかもしれません。

React Context ( server で利用できません)を使う代わりに、あるいはデータを props として渡す代わりに、fetchを使うか、 fetch のcache関数を使って、必要な components で同じデータを取得できます。これは同じデータの重複要求について心配することなく、 React は自動的にデータ要求をメモ化するようにfetchを拡張しているからです。そして、fetchが利用できない時には、cache関数を使用することができます。

React におけるmemoizationについてもっと学びましょう。

server 専用の Code を Client の環境から保つ

JavaScript modules は、Server と Client Components modules の両方で共有できるため、もともとは server 上で実行されることを意図していた code が、client に潜り込むことが可能です。

たとえば、次のようなデータ取得関数を考えてみましょう:

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()
}
lib/data.js
export async function getData() {
  const res = await fetch('https://external-service.com/data', {
    headers: {
      authorization: process.env.API_KEY,
    },
  })

  return res.json()
}

一見すると、getDataは server と client の両方で動作するように見えます。しかし、この関数にはAPI_KEYが含まれており、これは常に server 上でのみ実行されることを意図して書かれています。

環境 variable API_KEYNEXT_PUBLICで始まらないので、それは server 上でのみアクセス可能なプライベートな variable です。environment variables を client に漏らさないようにするために、Next.js はプライベートな environment variables を empty string に置き換えます。

その結果、getData()は client 上でインポートおよび実行できますが、期待通りには動作しません。そして、variable public ことで、関数を client 上で動作させることができますが、敏感な情報を client に晒すことは望ましくないかもしれません。

この種の意図しない client の server code の使用を防ぐために、私たちはserver-onlyパッケージを使用して、他の開発者が誤ってこれらの modules を Client Component に import すると、build 時に error を出すことができます。

server-onlyを使用するには、まずパッケージをインストールします:

Terminal
npm install server-only

その後、server 専用の code を含む任意のモジュールにパッケージを import します:

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()
}

現在、getData()をインポートする任意の Client Component は、このモジュールが server 上でのみ使用できることを説明する build 時の error を受け取ります。

対応するパッケージ client-onlyは、client 専用のコードを含むモジュールをマークするために使用できます。たとえば、 window object にアクセスするコードなどです。

サードパーティのパッケージとプロバイダーの使用

「Server Components は新しい React の機能であるため、エコシステム内のサードパーティパッケージやプロバイダーは、useStateuseEffectcreateContextのような Client 専用機能を使用する Component に対して"use client"ディレクティブを追加し始めています。

今日、Client 専用機能を使用するnpmパッケージの多くの Component はまだディレクティブを持っていません。これらのサードパーティ Component は、""use client"ディレクティブを持っているため、Client Components 内では期待通りに動作しますが、Server Components 内では動作しません。

たとえば、架空のacme-carouselパッケージをインストールしたとしましょう。このパッケージには<Carousel /> component が含まれています。この component はuseStateを使用していますが、まだ"use client"ディレクティブは持っていません。

あなたが Client Component 内で<Carousel />を使用すると、期待通りに動作します:

app/gallery.tsx
'use client'

import { useState } from 'react'
import { Carousel } from 'acme-carousel'

export default function Gallery() {
  let [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>
  )
}
app/gallery.js
'use client'

import { useState } from 'react'
import { Carousel } from 'acme-carousel'

export default function Gallery() {
  let [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>
  )
}

しかし、それを直接 Server Component 内で使用しようとすると、error が表示されます:

app/page.tsx
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>
  )
}
app/page.js
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 />が Client 専用の機能を使用していることを認識していないからです。

これを修正するには、Client 専用機能に依存するサードパーティ Component を自身の Client Components でラップすることができます:

app/carousel.tsx
'use client'

import { Carousel } from 'acme-carousel'

export default Carousel
app/carousel.js
'use client'

import { Carousel } from 'acme-carousel'

export default Carousel

さあ、あなたは <Carousel /> を直接 Server Component の中で使うことができます:

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>
  )
}
app/page.js
import Carousel from './carousel'

export default function Page() {
  return (
    <div>
      <p>View pictures</p>

      {/*  Works, since Carousel is a Client Component */}
      <Carousel />
    </div>
  )
}

私たちはあなたがほとんどのサードパーティ Component を包む必要はないと思います。それはおそらくあなたがそれらを Client Components 内で使用するからです。ただし、例外はプロバイダーで、それらは React の状態と context に依存しており、通常はアプリケーションの root で必要とされます。 サードパーティの context プロバイダーについて下記で詳しく学んでください

Context プロバイダーを使用する

Context プロバイダーは、通常、現在のテーマのようなグローバルな関心事を共有するために、アプリケーションの root 近くでレンダリングされます。 React context は、Server Components ではサポートされていないので、アプリケーションの root で context を作成しようとすると、error が発生します。

app/layout.tsx
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>
  )
}
app/layout.js
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>
  )
}

これを修正するには、あなたの context を作成し、それを Client Component 内部の render プロバイダーに適用します:

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>
}
app/theme-provider.js
'use client'

import { createContext } from 'react'

export const ThemeContext = createContext({})

export default function ThemeProvider({ children }) {
  return <ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>
}

あなたの Server Component は、それが Client Component としてマークされているので、プロバイダーを直接 render することができるようになります。

app/layout.tsx
import ThemeProvider from './theme-provider'

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

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <ThemeProvider>{children}</ThemeProvider>
      </body>
    </html>
  )
}

プロバイダーが root でレンダリングされると、app 内の他の全ての Client Components はこの context を消費することができるようになります。

Good to know: プロバイダをツリーで可能な限り深く render すべきですーThemeProviderが全ての <html> document ではなく{children}のみを囲っていることに注目してください。これにより、 Next.js は Server Components の静的部分を最適化するのが容易になります。

Library の作者へのアドバイス

同様に、library の作者が他の開発者が使用するためのパッケージを作成する際には、"use client"ディレクティブを使用してパッケージの client エントリーポイントをマークすることができます。これにより、パッケージのユーザーは、ラッピング境界を作成せずにパッケージ Component を直接 Server Components に import することが可能になります。

use client をツリーの深い部分で使用することにより、use client deeper in the treeあなたのパッケージを最適化することができます。これにより、インポートされた modules が Server Component モジュールグラフの一部になります。

いくつかのバンドラーは"use client"ディレクティブを削除する可能性があるということに注意が必要です。esbuild を構成して"use client"ディレクティブを含める方法の例は、React Wrap Balancer および Vercel Analytics のリポジトリで見つけることができます。

Client Components

Client Components をツリーの下に移動

Client JavaScript バンドルの size を減らすためには、Client Components を component ツリーの下部に移動することをお勧めします。

例えば、静的な要素(例:ロゴ、リンクなど)と、ステートを使用したインタラクティブな検索バーを持つ Layout を持つかもしれません。

全体の layout を Client Component にする代わりに、インタラクティブなロジックを Client Component(例えば<SearchBar />)に移動させ、layout を Server Component として保持します。これは、layout の全ての component Javascript を client に送らなくても済むということを意味します。

app/layout.tsx
// 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>
    </>
  )
}
app/layout.js
// 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 }) {
  return (
    <>
      <nav>
        <Logo />
        <SearchBar />
      </nav>
      <main>{children}</main>
    </>
  )
}

Server から Client Components への props の渡し方(シリアライズ)

もし Server Component でデータを fetch する場合、そのデータを Props として Client Components に渡したいかもしれません。 Server から Client Components へ渡される props は、 React がシリアライズ できる必要があります。

もし、Client Components がシリアライズできないデータに依存している場合は、第三者の library を用いて client 上でfetchデータを取得するか、または server 上でRoute Handlerを通じて取得することができます。

Interleaving Server and Client Components

Client と Server Components を交互に使う場合、UI を Component のツリーとして視覚化すると役立つかもしれません。root layoutから始めて、これは Server Component であり、その後、"use client"ディレクティブを追加することで、特定の Component のサブツリーを client で render することができます。

その client サブツリーの中でも、まだ Server Components をネストしたり、Server Actions を呼び出したりすることができますが、心に留めておくべきことがいくつかあります:

  • リクエスト-レスポンスのライフサイクル中、あなたの code は server から client に移動します。もし client で server 上のデータやリソースにアクセスする必要がある場合、あなたは新しい request を server にすることになります - 行き来するのではなく。
  • 新しい request が Server に対して行われると、すべての Server Components が最初にレンダリングされ、Client Components の中にネストされたものも含まれます。 レンダリングされた結果(RSC Payload)には、Client Components の位置への参照が含まれます。 その後、client 上で、React は RSC Payload を使用して server と Client Components を一つのツリーに整合させます。
  • Client Components は Server Components の後にレンダリングされるため、Client Component モジュールに Server Component を import することはできません(それは新たな request を server に戻すことを require します)。代わりに、Server Component をpropsとして Client Component に渡すことができます。以下の非対応パターンセクションと対応パターンセクションを参照してください。

サポートされていないパターン: Client Components への Server Components のインポート

次のパターンはサポートされていません。あなたは Client Component に Server Component を import することはできません:

app/client-component.tsx
'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 />
    </>
  )
}
app/client-component.js
'use client'

// You cannot import a Server Component into a Client Component.
import ServerComponent from './Server-Component'

export default function ClientComponent({ children }) {
  const [count, setCount] = useState(0)

  return (
    <>
      <button onClick={() => setCount(count + 1)}>{count}</button>

      <ServerComponent />
    </>
  )
}

サポートされるパターン:Server Components を Client Components へ Props として渡す

次のパターンがサポートされています。 Server Components を Client Component の prop として渡すことができます。

一般的なパターンは、React のchildrenプロップを使用して、Client Component に*"slot"*を作成することです。

以下の例では、<ClientComponent>children プロップを受け入れます:

app/client-component.tsx
'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}
    </>
  )
}
app/client-component.js
'use client'

import { useState } from 'react'

export default function ClientComponent({ children }) {
  const [count, setCount] = useState(0)

  return (
    <>
      <button onClick={() => setCount(count + 1)}>{count}</button>

      {children}
    </>
  )
}

<ClientComponent>は、childrenが最終的に Server Component の結果によって埋められることを知らない。<ClientComponent>の唯一の責任は、childrenが最終的にどこに配置されるかを決定することです。

親の Server Component では、<ClientComponent><ServerComponent>の両方を import でき、<ServerComponent><ClientComponent>の子として渡すことができます。

app/page.tsx
// 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>
  )
}
app/page.js
// 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>は server 上でレンダリングすることができ、それは<ClientComponent>が client 上でレンダリングされるよりもはるかに早いです。

Good to know:

  • "リフティングコンテンツアップ"のパターンは、親の"component"が再レンダリングされたときに、ネストされた子の"component"が再レンダリングされるのを回避するために使われてきました。
  • あなたはchildrenプロップに限定されません。任意のプロップを使用して JSX を渡すことができます。

当社サイトでは、Cookie を使用しています。各規約をご確認の上ご利用ください:
Cookie Policy, Privacy Policy および Terms of Use