⛅️

[Next.js v13][和訳] Server & Client Components

2022/10/29に公開

このページの翻訳記事になります。
https://beta.nextjs.org/docs/rendering/server-and-client-components

Server and Client Components

Server Components と Client Components によって、「従来のサーバーレンダリングのパフォーマンス向上」と「クライアントサイドのリッチな双方向性」を両立させながら、サーバー・クライアント両方の環境でアプリケーションを構築できるようになりました。

このページを読むことで、Server Components と Client Components の違いと、Next.js のアプリケーションでどのようにそれらを使うべきかを知ることが出来ます。

Server Components

app ディレクトリ内の全てのコンポーネントはデフォルトで React Server Components となっています。そして、それは特殊なファイルと併置されたコンポーネントを含んでいます。デフォルトで React Server Components になっているおかげで、追加の作業をすることなく、Server Components に適応でき、すぐに優れたパフォーマンスを実現します。

Why Server Components?

Server Components によって、開発者はサーバーのインフラストラクチャをより有効に活用できます。例えば、以前は クライアントの JavaScript バンドルサイズに影響を与えていた大きな依存関係を 完全にサーバーに残すことができるようになる ため、パフォーマンスが向上します。

ルーティングが始まると、Next.js と React ランタイムが読み込まれます。これは、キャッシュが可能でサイズも予測可能です。このランタイムは、アプリケーションが成長してもサイズが大きくなりません。さらに、ランタイムは非同期でロードされるため、サーバーからの HTML をクライアントで段階的に拡張できます。

Client Components

Client Components はクライアント上でレンダリングされます。 Next.js を使用すると、Client Components をサーバー上で事前にレンダリングし、クライアントでハイドレートすることもできます。

Convention

Client Components を使用するには、アプリ内にファイルを作成し、'use client' ディレクティブ をコードの先頭に追加します。

app/Counter.js
'use client';

import { useState } from 'react';

export default function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  );
}

コンポーネントが useStateuseEffect などの hooks を使用する場合にのみ、コンポーネントを 'use client' のように明示する必要があります。hooks に依存しないコンポーネントは、Server Components として自動的にレンダリングされるように、ディレクティブなしで残しておくことをお勧めします。 これによって、クライアント側の JavaScript を最小限に抑えることができます。

When to use Server vs. Client Components?

Server Components と Client Components のどちらを使うかという話になった場合、Client Components を使用する必要が生じるまでは、Server Components (app ディレクトリのデフォルト設定) を使用することをお勧めします。

次の表は、Server Components と Client Components の様々な使用例をまとめたものです。

したいこと Server Component Client Component
Fetch data ⚠️
Access backend resources (directly)
Keep sensitive information on the server (access tokens, API keys, etc)
Keep large dependencies on the server / Reduce client-side JavaScript
Add interactivity and event listeners (onClick(), onChange(), etc)
Use State and Lifecycle Effects (useState(), useReducer(), useEffect(), etc)
Use browser-only APIs
Use custom hooks that depend on state, effects, or browser-only APIs
Use React Class components

Recommendations

Data Fetching

Client Components でデータを取得することは可能ですが、クライアントでデータを取得する特別な理由がない限り、Server Components でデータを取得することをお勧めします。データ取得をサーバーに移行すると、パフォーマンスと UX が向上します。

Passing props from Server to Client Components (Serialization)

Server Components から Client Components に渡される props は、シリアライズ可能である必要があります。これは、関数、日付などの値を Client Components に直接渡すことができないということです。

Where is the Network Boundary?

app ディレクトリでは、Network Boundary は Server Components と Client Components の間にあります。これは、Boundary が getStaticProps/getServerSideProps と Page Components の間にある pages ディレクトリとは異なります。Server Components 内でフェッチされたデータは、 Client Components に渡されない限り、Network Boundary を越えないため、シリアル化する必要はありません。

Keeping Server-Only Code out of Client Components (Poisoning)

JavaScript モジュールは Server Components と Client Components の両方で共有できるため、サーバー上でのみ実行するということを意図したコードがクライアントに忍び込む可能性があります。

たとえば、以下のデータ フェッチ関数があります。

lib/data.js
export async function getData() {
  let res = await fetch("https://external-service.com/data", {
    headers: {
      authorization: process.env.API_KEY,
    },
  });

  return res.json();
}

一見すると、getData はサーバーとクライアントの両方で機能するように見えます。ただし、環境変数 API_KEY には NEXT_PUBLIC が prefix として付けられていないため、サーバー上でのみアクセスできるプライベート変数です。Next.js は、クライアントコード内のプライベート環境変数を空の文字列に置き換えて、安全な情報が漏洩するのを防いでいます。

その結果、クライアントで getData() をインポートして実行できても、期待どおりに動作しません。また、変数を public にすると、クライアントで関数が機能するようになりますが、機密情報が漏洩する可能性があります。なので、この関数はサーバー上でのみ実行されることを意図して作成しています。

この種の意図しないクライアントによるサーバー側のコードの使用を防ぐために、server-only パッケージを使用して、他の開発者が server-only が使われているモジュールのうちのどれか1つでも Client Components でインポートした場合に、ビルドエラーを吐かせることができます。

server-only を使用するには、インストールが必要です。

$ npm install server-only

インストールが完了したら、サーバー上で動かす想定の任意のモジュールにパッケージをインポートします。

lib/data.js
import "server-only";

export async function getData() {
  let resp = await fetch("https://external-service.com/data", {
    headers: {
      authorization: process.env.API_KEY,
    },
  });

  return resp.json();
}

これで、getData() をインポートする全ての Client Components は、「このモジュールがサーバー上でしか使用できない」というビルドエラーを受け取ります。

対応するパッケージ client-only を使用して、window オブジェクトにアクセスするコードなど、クライアントのみのコードを含むモジュールを明示できます。

Moving Client Components to the Leaves

アプリケーションのパフォーマンスを向上させるために、可能であれば Client Components を Component Tree の 枝葉 に移動することをお勧めします。

例えば、静的要素 (ロゴ、リンクなど) と状態を使用するインタラクティブな検索バーを含むレイアウトがあるとします。

レイアウト全体を Client Components にする代わりに、インタラクティブなロジック を Client Components (<SearchBar /> など) に移動し、レイアウトを Server Componentsとして保持します。これは、レイアウト内のすべてのコンポーネント(JavaScript)をクライアントに送信する必要がないということです。

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>
    </>
  );
}

Importing Server Components into Client Components

同じ Component Tree 内で、Server Components と Client Components は、交互に使うことができます。その裏では、React は両方の環境の作業をマージしてくれています。

ただし、React では、Client Components 内に Server Components をインポートすることに関して制限があります。 これは、Server Components がサーバーのみのコード (データベースまたはファイル システム ユーティリティなど) を含む可能性があるためです。

例えば、Server Components を Client Components に以下のようにインポートしても機能しません。

app/ClientComponent.js
'use client';

// ❌ This pattern will not work. You cannot import a Server
// Component into a Client Component
import ServerComponent from './ServerComponent';

export default function ClientComponent() {
  return (
    <>
      <ServerComponent />
    </>
  );
}

Server Components は Client Components の 子要素 または prop として渡すことができます。これを行うには、両方のコンポーネントを別の Server Components にラップします。例えば

app/page.js
// ✅ This pattern works. You can pass a Server Component
// as a child or prop of a Client Component.
import ClientComponent from "./ClientComponent";
import ServerComponent from "./ServerComponent";

// Pages are Server Components by default
export default function Page() {
  return (
    <ClientComponent>
      <ServerComponent />
    </ClientComponent>
  );
}

このパターンでは、React は、結果 (サーバーのみのコードを含まない) をクライアントに送信する前に、サーバー上で <ServerComponent /> をレンダリングする必要があると認識します。そして、Client Components は、その子要素は既にレンダリングされていると認識します。

このchildren prop を使用したパターンは layouts と pages には既に適用されているため、追加のラッパーコンポーネントを作成する必要はありません。

例えば、 <ClientLayout /> コンポーネントは <ServerPage /> コンポーネントを子要素として受け入れます。

app/dashboard/layout.js
'use client';

// The Dashboard Layout is a Client Component
export default function ClientLayout({ children }) {
  // Can use useState / useEffect here
  return (
    <>
      <h1>Layout</h1>
      {children}
    </>
  );
}
app/dashboard/page.js
// The Dashboard Page is a Server Component that will be
// passed to Dashboard Layout
export default function ServerPage() {
  return (
    <>
      <p>Page</p>
    </>
  );
}

API Reference

GitHubで編集を提案

Discussion