Next.js

Next.jsのuseContext入門:Providerでテーマと状態を共有する

この記事でわかること

Next.jsでuseContextを使い、テーマを共有する実践例を紹介。型付きProvider、専用Hook、更新と再描画、Pages RouterとApp Routerの配置の違いを解説します。

useContextは、祖先にあるProviderから値を受け取るReactのHookです。テーマや表示設定のように複数の部品が必要とする値を、中間の部品へ何段もpropsで渡さずに共有できます。状態そのものを自動で作る機能ではなく、状態の更新にはuseStateなどを組み合わせます。

この記事ではPages Router・TypeScriptを使い、テーマを切り替える例を作ります。React 18でも使えるContext.Providerの書き方を使用します。App Routerでの配置は後半で説明します。

テーマを共有するContextを実装する

Contextと専用Hookを定義する

ファイル:src/contexts/ThemeContext.tsx

import { createContext, useContext, useState } from 'react';
import type { PropsWithChildren } from 'react';

type Theme = 'light' | 'dark';
type ThemeValue = {
  theme: Theme;
  toggleTheme: () => void;
};

const ThemeContext = createContext<ThemeValue | undefined>(undefined);

export function ThemeProvider({ children }: PropsWithChildren) {
  const [theme, setTheme] = useState<Theme>('light');

  function toggleTheme() {
    setTheme(current => current === 'light' ? 'dark' : 'light');
  }

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

export function useTheme(): ThemeValue {
  const context = useContext(ThemeContext);
  if (context === undefined) {
    throw new Error('useThemeはThemeProviderの内側で使用してください');
  }
  return context;
}

createContext()へ渡す初期値は、対応するProviderが祖先にない場合のフォールバックです。ここではundefinedを使い、Providerの配置忘れを専用Hookで検出します。Contextはモジュールのトップレベルで一度定義し、コンポーネントの描画中に作り直しません。

テーマの値はProviderの状態です。toggleTheme()が値を更新すると、そのContextを読む子孫へ新しい値が伝わります。この例ではブラウザの保存領域は使用しないため、ページを完全に再読み込みすると初期値のlightへ戻ります。

Pages Routerの共通部分に配置する

ファイル:src/pages/_app.tsx

import type { AppProps } from 'next/app';
import { ThemeProvider } from '../contexts/ThemeContext';

export default function App({ Component, pageProps }: AppProps) {
  return (
    <ThemeProvider>
      <Component {...pageProps} />
    </ThemeProvider>
  );
}

既存の_app.tsxがある場合は、グローバルCSSやレイアウトなどの処理を残し、Providerで必要な範囲を囲みます。サンプルでファイル全体を上書きして既存の設定を失わないようにしてください。

同じProviderがマウントされたままのクライアント側ページ移動では、状態を共有できます。Providerをページごとに置いて作り直す場合や、ブラウザを再読み込みする場合は状態の寿命が変わります。

子コンポーネントから読み取り・更新する

ファイル:src/components/ThemeButton.tsx

import { useTheme } from '../contexts/ThemeContext';

export default function ThemeButton() {
  const { theme, toggleTheme } = useTheme();
  return (
    <button type="button" onClick={toggleTheme} aria-pressed={theme === 'dark'}>
      ダークテーマ: {theme === 'dark' ? '有効' : '無効'}
    </button>
  );
}

ファイル:src/pages/theme.tsx

import ThemeButton from '../components/ThemeButton';
import { useTheme } from '../contexts/ThemeContext';

export default function ThemePage() {
  const { theme } = useTheme();
  return (
    <main style={{
      padding: 24,
      backgroundColor: theme === 'dark' ? '#15201f' : '#ffffff',
      color: theme === 'dark' ? '#ffffff' : '#15201f',
    }}>
      <h1>Contextで共有するテーマ</h1>
      <p>現在のテーマ: {theme}</p>
      <ThemeButton />
    </main>
  );
}

npm run dev/themeへアクセスすると、ボタンとページが同じテーマを参照し、クリックで表示が切り替わります。ボタンへテーマをpropsで渡す必要はありません。

再描画と値の管理に注意する

Providerの値が変わると、Contextを読むコンポーネントは更新を受け取ります。値の比較にはObject.isが使われます。この例のvalueは新しいオブジェクトなので、Providerが別の理由で再描画された場合にも参照が変わります。

実際に不要な再描画が問題になったら、Contextの責務を分ける、Providerの配置を狭める、必要に応じてuseMemouseCallbackで値を安定させるなどを検討します。メモ化しても、テーマ自体が変わった際の利用側の更新は必要です。単に親から近い子へ渡すだけなら、propsのほうが流れを読み取りやすい場合もあります。

App Routerでの利用と認証情報

App RouterではProviderのファイルを'use client';で始め、必要なClient Componentを囲みます。Server Componentのレイアウトから、そのProviderを読み込んで子要素を渡すことはできます。ただしServer Component自身がuseContextでクライアントの状態を読む構成にはできません。

ユーザーの表示名などを共有する用途には使えますが、Contextはアクセス権限を保証する仕組みではありません。画面の状態を変更できることと、サーバー上のデータにアクセスできることを混同せず、認証・認可はサーバー側でも確認します。秘密のAPIキーをContextへ入れてブラウザへ渡すことも避けます。

関連記事と公式資料

Contextを広げすぎない設計

必要な範囲にProviderを置く

テーマや設定を必要とする部品を確認して、Providerを配置します。App RouterではProviderをClient Componentとして用意し、必要な範囲を包む構成を検討してください。

更新頻度の違う値を整理する

頻繁に変わる入力値と、ほとんど変わらない設定を同じContextへまとめると、利用側の更新範囲が広がる場合があります。実際の描画を確認し、役割に応じて分割するか判断します。

Next.jsとは・ルーターの違い・学習順を確認する

y.
WRITTEN BY

y_ymo10

SEの部屋で、JavaScript・TypeScript・React.js・Next.jsの開発ノートを公開しています。

ほかの開発ノートを読む →