はじめに
Reactでコンポーネントツリーをまたいでデータを共有したいとき、どのように実装していますか?
この記事では、ReactのContext APIについて、基本概念から実践的な使い方まで解説します。Props drillingの問題を解決し、より保守性の高いコードを書けるようになりましょう。
この記事で学べること
- Props drillingの問題点
- Context APIの基本構成と仕組み
- 実践的な実装例
- Context APIの適切な使い分け
Props drillingの問題点
まず、Context APIが解決する問題について見ていきます。
従来のprops渡しの課題
Reactでは、親コンポーネントから子コンポーネントへpropsでデータを渡します。しかし、深い階層のコンポーネントにデータを渡す場合、中間のコンポーネントを経由する必要があります。
この問題をProps drillingと呼びます。
具体例:3階層のコンポーネント
次のようなコンポーネント構造があるとします:
// 最上位のページコンポーネント
function UserProfilePage() {
const user = { name: "田中太郎", role: "管理者" };
return <ProfileContainer user={user} />;
}
// 中間コンポーネント(userを使わないが受け渡しが必要)
function ProfileContainer({ user }) {
return (
<div className="container">
<UserInfo user={user} />
</div>
);
}
// 実際にuserを使うコンポーネント
function UserInfo({ user }) {
return (
<div>
<p>名前: {user.name}</p>
<p>役割: {user.role}</p>
</div>
);
}
この例では、ProfileContainerはuserデータを使いませんが、UserInfoに渡すためだけに受け取っています。
Props drillingの流れ
黄色のProfileContainerは、データを中継するだけで実際には使っていません。これがProps drillingの問題点です。
Props drillingによる弊害
- 中間コンポーネントの肥大化: 使わないpropsを受け渡すだけのコードが増える
- 保守性の低下: データ構造の変更時に複数のコンポーネントを修正する必要がある
- 再利用性の低下: 中間コンポーネントが特定のpropsに依存してしまう
- 可読性の低下: データの流れが複雑になり、追跡が困難になる
Context APIの基本構成
Context APIは、この問題を解決するための仕組みです。コンポーネントツリー全体でデータを共有できるようになります。
3つの構成要素
Context APIは3つの要素で構成されています:
1. Context オブジェクト
createContext()で作成される、データの「入れ物」です。
import { createContext } from 'react';
const UserContext = createContext(null);
デフォルト値を設定できますが、通常はProviderで実際の値を提供します。
2. Provider(提供者)
Contextの値を設定し、配下のコンポーネントツリーに提供します。
<UserContext.Provider value={user}>
{/* ここに配置されたコンポーネントはuserにアクセス可能 */}
</UserContext.Provider>
valueプロパティで実際のデータを渡します。
3. Consumer(消費者)
Contextの値を受け取る側です。関数コンポーネントではuseContext()フックを使います。
import { useContext } from 'react';
function UserInfo() {
const user = useContext(UserContext);
return <p>{user.name}</p>;
}
Context APIのデータの流れ
Providerでラップされたコンポーネントツリー内であれば、どの階層からでもuseContext()でデータにアクセスできます。
Context APIを使った場合の構造
ProfileContainerは何もpropsを受け取る必要がなくなり、UserInfoが直接Contextからデータを取得します。
実装例:ユーザー情報の共有
実際にContext APIを使った実装を見ていきましょう。
ステップ1: Contextの作成
まず、Contextオブジェクトを作成します。
// contexts/UserContext.js
import { createContext } from 'react';
export const UserContext = createContext(null);
別ファイルにすることで、複数のコンポーネントからimportして使えるようにします。
ステップ2: Providerの設定
アプリケーションの上位コンポーネントでProviderを設置します。
// App.js
import { UserContext } from './contexts/UserContext';
function App() {
const user = {
name: "田中太郎",
role: "管理者",
email: "tanaka@example.com"
};
return (
<UserContext.Provider value={user}>
<UserProfilePage />
</UserContext.Provider>
);
}
Providerのvalueに渡したuserオブジェクトが、配下の全コンポーネントで利用可能になります。
ステップ3: useContextでのデータ取得
必要なコンポーネントでuseContext()を使ってデータを取得します。
// components/UserInfo.js
import { useContext } from 'react';
import { UserContext } from '../contexts/UserContext';
function UserInfo() {
const user = useContext(UserContext);
if (!user) {
return <p>ユーザー情報がありません</p>;
}
return (
<div>
<h2>ユーザー情報</h2>
<p>名前: {user.name}</p>
<p>役割: {user.role}</p>
<p>メール: {user.email}</p>
</div>
);
}
中間コンポーネントを経由せず、直接userデータにアクセスできています。
ステップ4: カスタムフックでのラップ
useContext()を直接使うのではなく、カスタムフックでラップするのがベストプラクティスです。
// contexts/UserContext.js
import { createContext, useContext } from 'react';
const UserContext = createContext(null);
// カスタムフック
export function useUser() {
const context = useContext(UserContext);
if (context === null) {
throw new Error('useUser must be used within a UserProvider');
}
return context;
}
export const UserContext = UserContext;
コンポーネントでの使用がシンプルになります:
import { useUser } from '../contexts/UserContext';
function UserInfo() {
const user = useUser(); // useContext(UserContext)の代わり
return (
<div>
<p>名前: {user.name}</p>
</div>
);
}
カスタムフックのメリット
- エラーハンドリング: Provider外での使用を検出できる
- 実装の隠蔽: Contextの実装詳細を隠せる
- 型安全性の向上: TypeScriptで型推論が効きやすくなる
- 将来的な変更が容易: 内部実装を変えてもコンポーネント側は変更不要
まとめ
Context APIは、Reactでコンポーネント間のデータ共有を実現する強力な仕組みです。
Context APIのメリット
- Props drillingを解消できる
- コードの保守性が向上する
- コンポーネントの再利用性が高まる
- データの流れが明確になる
Context APIのデメリット
- 不適切に使うとパフォーマンスが低下する
- データの流れが暗黙的になり、デバッグが難しくなる場合がある
- テストが複雑になる
ベストプラクティス
- カスタムフックでラップする: エラーハンドリングと型安全性を向上
- Contextを適切に分割する: 関連性の低いデータは別々のContextに
- useMemoで最適化する: 不要な再レンダリングを防ぐ
- 適材適所で使う: すべてをContextで管理しない