はじめに
Reactを初めて触ったとき、propsでデータを渡す仕組みはとてもシンプルで直感的だと感じました。 しかし、アプリの規模が大きくなり、コンポーネントの階層が深くなるにつれて、避けて通れない問題――props drilling――が現れます。
Props Drillingとは?
Props Drillingとは、上の画像のように必要なコンポーネントにデーターを渡すために、そのデータを使わない中間コンポーネントを経由してpropsを何度も渡さなければならない状態をProps Drillingと言います。
Props Drillingの例
以下は「theme」を最下層コンポーネントに渡しているシンプルな例です。
// App.jsx
import React, { useState } from "react";
import Page from "./Page.jsx";
function App() {
const [theme, setTheme] = useState("light");
return (
<div>
<h1>Props Drilling の例</h1>
{/* 必要なデータを最下層まで渡す */}
<Page theme={theme} setTheme={setTheme} />
</div>
);
}
export default App;
// Page.jsx
import React from "react";
import Content from "./Content.jsx";
function Page({ theme, setTheme }) {
return (
<div>
{/* このコンポーネント自体はthemeを使わない */}
<Content theme={theme} setTheme={setTheme} />
</div>
);
}
export default Page;
// Content.jsx
import React from "react";
import Footer from "./Footer.jsx";
function Content({ theme, setTheme }) {
return (
<div>
<p>ここはContentエリア</p>
{/* さらに下のFooterへ渡す */}
<Footer theme={theme} setTheme={setTheme} />
</div>
);
}
export default Content;
// Footer.jsx
import React from "react";
function Footer({ theme, setTheme }) {
return (
<footer>
<p>現在のテーマ: {theme}</p>
<button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
テーマを切り替える
</button>
</footer>
);
}
export default Footer;
このように、一番下のコンポーネントに渡すためだけに、最上階層から宣言した state を 中間コンポーネント全てに props として渡す必要があります。
次は、同じ構成(App → Page → Content → Footer)のまま Context API を使って “中継なし” で Footer から直接値を取得する例を見て、props drilling をどう回避できるかを確認します。
props drillingを回避するContext APIの例と使い方
React の Context API を使えば、props drilling を避けながらアプリ全体で状態を共有できます。使い方は大きく 3ステップ です。以下の例は theme 状態を Footer から直接取得・更新するシンプルなパターンです。
1. Context を作成する
まず、createContext() でコンテキストを作成します。
デフォルト値として theme と setTheme を用意します。
// ThemeContext.jsx
import { createContext } from "react";
export const ThemeContext = createContext({
theme: "light",
setTheme: () => {},
});
2. Provider で値を配布する
作成したコンテキストの Provider で、共有したい値を value に渡します。
この Provider 配下にあるコンポーネントは、どこからでも同じ値を取得できます。
// App.jsx
import React, { useState } from "react";
import { ThemeContext } from "./ThemeContext.jsx";
import Page from "./Page.jsx";
function App() {
const [theme, setTheme] = useState("light");
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<div>
<h1>Context API の例(Props Drilling回避)</h1>
<Page />
</div>
</ThemeContext.Provider>
);
}
export default App;
3. 中間コンポーネントは props 不要
Page や Content のような中間コンポーネントでは、theme や setTheme を受け渡す必要がありません。
// Page.jsx
import React from "react";
import Content from "./Content.jsx";
function Page() {
return (
<div>
<Content />
</div>
);
}
export default Page;
// Content.jsx
import React from "react";
import Footer from "./Footer.jsx";
function Content() {
return (
<div>
<p>ここはContentエリア</p>
<Footer />
</div>
);
}
export default Content;
4. useContext で値を直接取得
一番下の Footer コンポーネントでは、useContext を使って ThemeContext から値を直接取得します。これにより、props drilling を完全に回避できます。
// Footer.jsx
import React, { useContext } from "react";
import { ThemeContext } from "./ThemeContext.jsx";
function Footer() {
const { theme, setTheme } = useContext(ThemeContext);
const next = theme === "light" ? "dark" : "light";
return (
<footer>
<p>現在のテーマ: {theme}</p>
<button
aria-label={`テーマを${next}に切り替える`}
onClick={() => setTheme(next)}
>
テーマを{next}に切り替える
</button>
</footer>
);
}
export default Footer;
このようにContextAPIを使いますとより簡単にprops drillingを回避することができます!
。
。
(終)
。
。
であればこの記事を投稿する意味はないはずです。当然ですが、このContextAPIにも大きい問題はあります。
Context APIの限界:Provider Hell
Context API は確かに props drilling を回避できる便利な仕組みですが、状態の種類が増えると Provider Hell(プロバイダ地獄) と呼ばれる状況に陥ることがあります。
Provider Hell とは?
複数の状態(テーマに加えてユーザー情報、通知設定など)を Context API で管理すると、それぞれに Provider を用意する必要があります。この Provider をネストしていくと、コードがどんどん深くなり、可読性・保守性が著しく低下します。
Provider Hellの例
// ThemeContext.jsx
import { createContext } from "react";
export const ThemeContext = createContext();
// UserContext.jsx
import { createContext } from "react";
export const UserContext = createContext();
// NotificationContext.jsx
import { createContext } from "react";
export const NotificationContext = createContext();
// App.jsx
import React, { useState } from "react";
import { ThemeContext } from "./ThemeContext.jsx";
import { UserContext } from "./UserContext.jsx";
import { NotificationContext } from "./NotificationContext.jsx";
import Page from "./Page.jsx";
function App() {
const [theme, setTheme] = useState("light");
const [user, setUser] = useState({ name: "Taro" });
const [notifications, setNotifications] = useState([]);
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<UserContext.Provider value={{ user, setUser }}>
<NotificationContext.Provider value={{ notifications, setNotifications }}>
<Page />
</NotificationContext.Provider>
</UserContext.Provider>
</ThemeContext.Provider>
);
}
export default App;
このように、機能が増えると最上位に Provider が積み上がり、可読性が低下します。もちろん、Provider の数を減らすために 1 つの Context にすべての状態(theme / user / notifications など)を詰め込む ことも可能で、動作自体に問題はありません。
しかしその場合、どれか 1 つの値変更でも購読側が広範囲に再レンダリング されやすく、結果的に保守性が下がる——まさに本末転倒 です。
終わりに
ここまで見てきたように、Context API は props drilling を避けられる便利な仕組みですが、
アプリの規模や機能の増加によって状態の種類や更新頻度が増えると Provider Hell や 不要な再レンダリングといった課題が発生します。
- 状態が多くなるほど Provider が増える
- 1つの Context にまとめても保守性やパフォーマンスが悪化する
- 状態の変更履歴を追う機能や、デバッグ支援ツールが不足している
こうした理由から、現場の多くのプロジェクトでは よりスケーラブルで予測可能な状態管理 が求められてきました。そのニーズによって登場したのが「Recoil」、「Zustand」、「Redux」などの状態管理ライブラリーです。
次回は、その中の一つである Redux を取り上げ、Context API の限界をどのように克服するのかを解説します。