てらブログ

てらブログ

日々の学習の備忘録

Next.js(Using App Router)のドキュメントを読んでServer Componentsの要点整理してみた

目的

Next.js(Using App Router)を読んで、App Routerと、Server Components / Client Componentsの概要理解と使用方法を確認したい

参照:
Docs | Next.js

主な特徴

Feature description
Routing Server Componentsの上に構築されたファイルシステムベースのルーターで、レイアウト、ネスト化されたルーティング、ロードステート、エラー処理などをサポートしています。
Rendering クライアント・コンポーネントとサーバー・コンポーネントによるクライアントサイドおよびサーバーサイド・レンダリング。Next.jsによるサーバー上の静的レンダリングと動的レンダリングでさらに最適化。EdgeおよびNode.jsランタイムでのストリーミング。
Data Fetching React Componentsのasync/awaitサポートと、ReactとWeb Platformに沿ったfetch()s APIにより、データ取得を簡素化。

サーバーコンポーネント

Server Componentsを使えば、開発者はサーバーのインフラをより有効に活用できる。例えば、データ取得をサーバーに移し、データベースに近づけることができる。また、以前はクライアントのJavaScriptバンドルのサイズに影響を与えていた大きな依存関係をサーバーに残すことができ、パフォーマンスの向上につながる。
App Router 内のすべてのコンポーネントはデフォルトで Server Components になっている。

クライアントコンポーネント

クライアントコンポーネントを使うと、アプリケーションにクライアントサイドのインタラクティブ機能を追加できる。Next.jsでは、クライアントコンポーネントはサーバー上でプリレンダリングされ、クライアント上でハイドレーションされる。クライアントコンポーネントは、Pages Router のコンポーネントと同じようなものだと考えてOK。
'use client'をファイルの一番上に挿入することで、クライアントコンポーネントとして扱われる。
また、use client "は、すべてのファイルで定義する必要はない。クライアント・モジュールの境界は、インポートされたすべてのモジュールがクライアント・コンポーネントとみなされるために、"エントリーポイント "で一度だけ定義する必要がある。

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

クライアントコンポーネントユースケースが決まるまで、サーバーコンポーネント(appディレクトリのデフォルト)を使うことを推奨されている。
サーバーコンポーネントとクライアントコンポーネントのさまざまな使用例のまとめ
Getting Started: React Essentials | Next.js

使用の際の詳細パターン

クライアント・コンポーネントの末端への移動

アプリケーションのパフォーマンスを向上させるには、クライアント・コンポーネントを可能な限りコンポーネント・ツリーの末端に移動させることが推奨される。
たとえば、静的な要素(ロゴ、リンクなど)を持つレイアウトと、状態を使用するインタラクティブな検索バーがあるとする。レイアウト全体をクライアントコンポーネントにする代わりに、インタラクティブロジックをクライアントコンポーネントなど)に移動し、レイアウトをサーバコンポーネントとして維持する。これにより、レイアウトのすべてのコンポーネント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>
    </>
  )
}

クライアント・コンポーネントとサーバー・コンポーネントの構成

サーバーコンポーネントとクライアントコンポーネントは、同じコンポーネントツリーで組み合わせることができる。
裏側では、Reactは以下のようにレンダリングを処理している:

 →これには、クライアントコンポーネントの中にネストされたサーバーコンポーネントも含まれる。
 →この段階で遭遇したクライアント・コンポーネントはスキップされる。

※Next.jsでは、最初のページロード時に、上記のステップでレンダリングされたサーバーコンポーネントとクライアントコンポーネントの両方が、HTMLとしてサーバー上にプリレンダリングされる。

クライアント・コンポーネントの中にサーバー・コンポーネント入れ子にする

サーバーコンポーネントをクライアントコンポーネントにインポートすることはできない。

'use client'
 
// This pattern will **not** work!
// You cannot import a Server Component into a Client Component.
import ExampleServerComponent from './example-server-component'
 
export default function ExampleClientComponent({
  children,
}: {
  children: React.ReactNode
}) {
  const [count, setCount] = useState(0)
 
  return (
    <>
      <button onClick={() => setCount(count + 1)}>{count}</button>
 
      <ExampleServerComponent />
    </>
  )
}

サーバーコンポーネントをpropとしてクライアントコンポーネントに渡すことが推奨されている。
以下のアプローチでは、レンダリングは切り離され、独立してレンダリングすることができる。

// This pattern works:
// You can pass a Server Component as a child or prop of a
// Client Component.
import ExampleClientComponent from './example-client-component'
import ExampleServerComponent from './example-server-component'
 
// Pages in Next.js are Server Components by default
export default function Page() {
  return (
    <ExampleClientComponent>
      <ExampleServerComponent />
    </ExampleClientComponent>
  )
}

サーバからクライアント・コンポーネントへの小道具の受け渡し(シリアライズ

サーバからクライアントコンポーネントに渡されるpropは、シリアライズ可能である必要がある。つまり、関数や日付などの値を直接クライアント・コンポーネントに渡すことはできない。

サーバー専用コードをクライアント・コンポーネントから排除する(ポイズニング)

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の前にNEXT_PUBLICが付けられていないため、サーバーでのみアクセスできるプライベート変数になっている。Next.jsでは、セキュアな情報の漏えいを防ぐため、クライアントコードではプライベートな環境変数を空文字列に置き換えている。
結果、getData()をインポートしてクライアント上で実行しても、期待通りに動作しない。また、変数をpublicにすることで、この関数はクライアント上で動作するようになるが、その場合は機密情報が漏れることになってしまう。
つまり、この関数はサーバー上でのみ実行されることを想定して書かれている。

サーバー専用パッケージ

上記のような意図しないクライアントによるサーバーコードの利用を防ぐために、サーバー専用パッケージを使用することで、他の開発者が誤ってこれらのモジュールをクライアント・コンポーネントにインポートした場合に、ビルド時にエラーを表示することができる。
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()
}

データのフェッチ

クライアント・コンポーネントでデータをフェッチすることも可能だが、特別な理由がない限り、サーバー・コンポーネントでデータをフェッチすることが推奨される。データ取得をサーバーに移すことで、パフォーマンスとユーザーエクスペリエンスが向上する。

コンテキスト

コンテキストのプロバイダは、グローバルな関心事を共有するために、アプリケーションのルート付近にレンダリングされるのが一般的。しかしServer Components ではコンテキストがサポートされていないため、アプリケーションのルートでコンテキストを作成しようとするとエラーになる。
これを解決するには、コンテキストを作成し、そのプロバイダをクライアント・コンポーネントの中にレンダリングする:

app/theme-provider.tsx
'use client'
 
import { createContext } from 'react'
 
export const ThemeContext = createContext({})
 
export default function ThemeProvider({ children }) {
  return <ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>
}

サーバーコンポーネントは、クライアントコンポーネントとしてマークされているので、プロバイダを直接レンダリングできるようになる:

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

プロバイダがルートにレンダリングされると、アプリ全体の他のすべてのクライアント・コンポーネントがこのコンテキストを利用できるようになる。