ReactのContextをちゃんと理解する

公開日:

タグ:

  • #Next.js
  • #React

久々にNext.jsを触ったところContextについての理解が足りなかったのでメモ。
(執筆日 2026年2月3日時点、React19の情報をベースにしています。 )

Contextとは

ReactではPropsを用いてツリー内で情報を受け渡すことが可能。しかしさまざまなコンポーネントで同じPropsを共有していると、その共通の先祖要素まで情報を渡していかなければならず、受け渡しが冗長で不便になる。これを「“props の穴掘り作業 (prop drilling)”」と呼ぶ。

Contextはこの冗長なデータの受け渡しをせず、中間のコンポーネントを経由せずに参照できる仕組み。

Contextはデータを集約する箱

Contextを使用することで親コンポーネントの配下にあるツリー全体にデータを受け渡すことができる共通の箱として機能する。この箱の中にあるコンポーネントはどこからでもアクセスが可能になる。
※ ただし、この箱は Providerで囲った範囲の中だけ で有効なスコープ付きの箱。

以前は Provider や Consumer を明示的に指定する必要があったが、バージョン19ではだいぶ簡略化されたので、直近の中では説明されてないことも多い。用語として以下は覚えておくと分かりやすかった。

Contextオブジェクト

Contextのコアになる部分。情報を取りまとめておく部分。

Providerコンポーネント

Context内の情報を子コンポーネントへの情報の提供(Provide)をするコンポーネント。

Consumerコンポーネント

Providerコンポーネント内に属する、useContext()でcontextの情報を受け取っているコンポーネントのことを呼ぶ。(バージョン19での説明には出てこないが以前は指定する必要があった。意味合いだけ覚えておくと良い。)

Contextの使い方を理解する

今回は例としてハンバーガーメニューを作成。

以下のようなレイアウトで、メニュー本体であるDrawerコンポーネントとHeaderコンポーネント内の開閉ボタンに処理を渡すことを想定し、実装を考える。

'use client';

import { Drawer } from '@/components/Drawer';
import { Footer } from '@/components/Footer/Footer';
import { Header } from '@/components/Header/Header';
import type { Metadata } from 'next';

export const metadata: Metadata = {
  title: 'Create Next App',
  description: 'Generated by create next app',
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="ja">
      <body>
        <Header />
        <main>
          {children}
        </main>
        <Drawer />
        <Footer />
      </body>
    </html>
  );
}

1. Contextを作成する

まずは DrawerContext.tsx というファイルを用意し、データを受け渡すためのContextを作成。作成するにはReactの createContext を使用する。

'use client';

import { createContext, useContext, RefObject } from 'react';

// ドロワーメニューのコンテキストの型定義
interface DrawerContextValue {
  dialogRef: React.RefObject<HTMLDialogElement | null>;
  isOpen: boolean;
  open: () => void;
  close: () => void;
}

// ドロワーメニューのコンテキストを作成
export const DrawerContext = createContext<DrawerContextValue | null>(null);

// カスタムフックを用意
export function useDrawerContext() {
  const context = useContext(DrawerContext);
  if (!context) {
    throw new Error('Drawer components must be used within a DrawerProvider');
  }
  return context;
}

createContextを使用してDrawerContext というContextオブジェクトを作成。createContextの初期値には null を指定する。 DrawerContext.tsx内では、実装で必要になる情報や関数をまとめて定義する。この Context は、配下のコンポーネントから共通で参照できる共有ポイントとして機能する。

今回はメニューの開閉を行うための関数や、DOM を参照するための情報を用意した。

~

// カスタムフックを用意
export function useDrawerContext() {
  const context = useContext(DrawerContext);
  if (!context) {
    throw new Error('Drawer components must be used within a DrawerProvider');
  }
  return context;
}

このファイル内に useDrawerContextというカスタムフックも用意しておく。

こうすることで、Context の取得方法を一箇所に集約できる。 Context の型やエラーハンドリングを意識せず、安全に利用できるようになる。

2. Contextを使用してコンポーネントに情報を受け渡す

'use client';

import { useDrawerContext } from '@/contexts/DrawerContext';

// ドロワーメニュー本体
export function Drawer() {
  const { close, isOpen, dialogRef } = useDrawerContext();
  return (
    <dialog ref={dialogRef} id="drawer" className="drawer">
      <div>ドロワーメニューの内容</div>
      <button 
        type="button"
        onClick={close}
        aria-controls="drawer"
        aria-expanded={isOpen}
        aria-label="メニューを閉じる"
      >
        Close
      </button>
    </dialog>
  );
}

先ほど作ったuseDrawerContextを使用してDrawerContext内の情報を呼び出す。 (Contextを呼び出しているのでこれがConsumerコンポーネントとなる。) dialogRefにdialogタグの情報を送り、メニューの閉じるボタンがクリックされたときには close を実行する。

3. 親要素となるProviderコンポーネントを作成する

'use client';

import { DrawerContext } from '@/contexts/DrawerContext';
import { useCallback, useRef, useState } from 'react';

// ドロワーメニューのProviderコンポーネント
export function DrawerProvider({ children }: { children: React.ReactNode }) {
  const dialogRef = useRef<HTMLDialogElement | null>(null);

  const [isOpen, setIsOpen] = useState(false);

  const open = useCallback(() => {
    if (dialogRef.current) {
      dialogRef.current.showModal();
      setIsOpen(true);
    }
  }, []);

  const close = useCallback(() => {
    if (dialogRef.current) {
      dialogRef.current.close();
      setIsOpen(false);
    }
  }, []);

  const contextValue = {
    dialogRef: dialogRef,
    isOpen: isOpen,
    open: open,
    close: close,
  };

  return (
    // React 18/19 両対応
    <DrawerContext.Provider value={contextValue}>
      {children}
    </DrawerContext.Provider>
  );
}

メニューのコンポーネントができたので、次はDrawerContextを使用するための親コンポーネントとなるProviderコンポーネント作成。 DrawerContext.tsx で指定したopen、closeを実行した際の処理、isOpen、dialogRefの設定を記述する。

この記事ではバージョン18,19どちらでも対応できるように DrawerContext.Provider と記述している。

バージョン18まではルートとなるコンポーネントには Context.Provider を使う必要があったが、バージョン19からは Context そのものを渡すだけでOKになっている。将来のバージョンでは <Context.Provider> が非推奨にする予定なので注意が必要。 *1

*1 <Context> がプロバイダに  | React v19 – React

4. ProviderコンポーネントでContextを使用したい部分を囲む

import { Drawer } from '@/components/Drawer';
import { DrawerProvider } from '@/components/DrawerProvider';
import { Footer } from '@/components/Footer/Footer';
import { Header } from '@/components/Header/Header';
import type { Metadata } from 'next';

export const metadata: Metadata = {
  title: 'Create Next App',
  description: 'Generated by create next app',
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="ja">
      <body>
	<DrawerProvider>
          <Header />
          <Drawer />
        </DrawerProvider>
        <main>
          <div className="container">{children}</div>
        </main>
        <Footer />
      </body>
    </html>
  );
}

今回はDrawerとHeaderの中でDrawerContextの情報を使用するので、DrawerProviderで二つを囲むように設定する。

5. Headerコンポーネント内で useDrawerContext を使用してみる

'use client';
import { Nav } from '@/components/Nav';
import { useDrawerContext } from '@/contexts/DrawerContext';
import styles from './Header.module.scss';

export function Header() {
  const { open, isOpen } = useDrawerContext();
  return (
    <header className={styles.header}>
      <h1 className={styles.name}>
        <a href="/">
          <span>yskm_dev</span>
        </a>
      </h1>
      <button
        type="button"
        className={styles.menuButton}
        aria-label="メニューを開く"
        aria-expanded={isOpen}
        aria-controls="drawer"
        onClick={open}
      >
        <span></span>
        <span></span>
      </button>
    </header>
  );
}

Headerコンポーネント内のメニューを開くボタンにも処理を設定するため、 useDrawerContext で情報を取得して設定していく。これでPropsでの受け渡しなどせずに、メニューの開閉ができるようになる。

実際に使用してみて感じたこと

コンポーネント同士で受け渡しが発生して煩雑化した際には便利だが、最初は実装方法に迷ってしまった。以下を注意しておくと良いかもしれない。

ロジックはなるべくProviderコンポーネント内に持たせる

子コンポーネント(Consumerコンポーネント)側に処理を持たせてしまうとそもそもContextを使う意味が薄れてしまうし、実装も煩雑になる。Providerコンポーネントに処理を集約させて、子コンポーネントは呼び出しと実行に注力させる方が良い。それも込みで設計を進めるとコードも整理されて良い感じだった。

使い所の注意点

サイトの各所で受け渡しが必要になる場合はContextの使用を検討した方が良いが、コンポーネント単位で完結するなら使用しなくて良い。簡単なPropsの受け渡しで済むならそちらを優先する。

また、Providerコンポーネントの情報が変更されるとConsumerコンポーネントの再レンダリングが発生するのでその辺りも注意が必要そうだと感じた。

参考リンク

一覧へ戻る