Server ComponentでContext APIを使う

こんにちは、アスクルでフロントエンドエンジニアをしているスガイです。
皆様App RouterやReact Server Componentとは仲良くできていますか?
Next.jsでのフロントエンド開発をするにあたり、よりよいUX実現のためにApp RouterそしてReact Server Componentへの理解が大いに求められる昨今です。
本記事ではその中でもServer Component内でContext APIを利用する方法について解説します。

前提:Server Component内では直接Context APIを利用できない

hooksが利用できない等の制約からコンポーネントの役割・機能に応じてServer ComponentとClient Componentを使い分ける必要があるのは、皆様ご承知おきのとおりかと思います。
とはいえ、Client Componentの中には原則Server Componentを配置できないため、親コンポーネントとなるlayout.tsxやpage.tsxはとりあえずServer Componentにしておきたいところです。
ここで1つ問題が発生します。アプリケーション全体で扱う値をContext APIやReduxなどで管理したい場合です。
アプリケーション全体で扱う値なわけなので、Providerはlayout.tsxやpage.tsxなどの親コンポーネントに配置したい。しかし、ProviderはServer Componentでは利用できません。

// app/layout.tsx
// Server ComponentではContext APIは直接利用できない(もちろんuseStateも利用できない)ので、以下の記述はエラーになる

import { createContext, ReactNode, useState } from 'react';

// 言語設定のContextを例にします
type Language = 'ja' | 'en';
interface LanguageContextType {
  language: Language;
  setLanguage: (language: Language) => void;
}

const defaultValue: LanguageContextType = {
  language: 'ja',
  setLanguage: () => {},
};
const LanguageContext = createContext<LanguageContextType>(defaultValue);

export default function RootLayout({ children }: { children: ReactNode }) {
  const [language, setLanguage] = useState<Language>('ja');
  return (
    <html lang="ja">
      <body>
        <LanguageContext value={{ language, setLanguage }}>
          {children}
        </LanguageContext>
      </body>
    </html>
  );
}

解決方法:Context Providerを置いたClient Componentを作成し、そのコンポーネントをServer Componentに置く

Server Componentの中ではContext APIを直接利用できないので、まずはContext APIを利用するために次のようなClient Componentを作成します。

// context/LanguageContextProvider.tsx
'use client';
import { createContext, ReactNode, useState } from 'react';

export type Language = 'ja' | 'en';
interface LanguageContextType {
  language: Language;
  setLanguage: (language: Language) => void;
}

const defaultValue: LanguageContextType = {
  language: 'ja',
  setLanguage: () => {},
};
export const LanguageContext = createContext<LanguageContextType>(defaultValue);

const LanguageContextProvider = ({ children }: { children: ReactNode }) => {
  const [language, setLanguage] = useState<Language>('ja');
  return (
    <LanguageContext value={{ language, setLanguage }}>
      {children}
    </LanguageContext>
  );
};

export default LanguageContextProvider;

上記のClient Componentをlayout.tsxに配置します。

// app/layout.tsx
// Server Componentの中で直接Context APIを利用していないのでエラーにならない
import { ReactNode } from 'react';
import LanguageContextProvider from './context/LanguageContextProvider';

export default function RootLayout({
  children,
}: {
  children: ReactNode
}) {
  return (
    <html lang="ja">
      <body>
        <LanguageContextProvider>{children}</ContextProvider>
      </body>
    </html>
  )
}

このようにすることで、layout.tsxをServer Componentとしながらアプリケーション全体で扱う値をContext APIで管理できるようになります。

余談

この「子要素をネスト可能なClient ComponentをServer Componentに配置する」という方法には別の使い方もあります。
レアケースかとは思いますが、Client Componentの一部分をServer Componentにしたいということも開発の中ではあるかもしれません。その場合も次のようにServer Componentにしたい部分をchildrenとして受け取るClient Componentを作成し、そのコンポーネントをServer Componentに配置することで実現できます。

// app/page.tsx
import MyClientComponent from './MyClientComponent';
import MyServerComponent from './MyServerComponent';

export default function MyPage() {
  return (
    <div>
      <MyClientComponent>
        <MyServerComponent />
      </MyClientComponent>
    </div>
  );
}

この場合でも MyServerComponent はServer Componentとして扱われます。
イベントハンドラを設定しなければいけない部分やhooksを利用したい部分だけをClient Componentとしつつ、そうではない部分をServer Componentにすることでクライアントサイドの描画コストを減らすといった最適化が可能です。

Server-side RenderingやReact Server Component等を筆頭に、描画を最適化する手段というのは多数存在します。
必要なのはその時々で適切な方法を選択することであり、豊富な選択肢を用意しておくことがその助けとなるのは間違いありません。
本記事がその1つとなれば幸いです。

ASKUL Engineering BLOG

2021 © ASKUL Corporation. All rights reserved.