Props drillingとは?ReactのContext APIで解決する方法を初心者向けに解説
生徒
「Reactでデータを渡すときに、propsをいくつもコンポーネントに渡していたら、だんだんコードがごちゃごちゃしてきました。これって普通のことですか?」
先生
「それはProps drillingという問題です。コンポーネントの階層が深くなるほど起きやすく、多くの初心者が悩むポイントです。」
生徒
「どうすれば解決できますか?」
先生
「Context APIを使えばスッキリ解決できます。実際のコードを見ながら、一緒に確認していきましょう!」
1. Props drillingとは何か?問題の本質を理解しよう
ReactでWebアプリを作るとき、画面の部品を「コンポーネント」という単位に分けて作成します。コンポーネント同士でデータを受け渡すときに使うのがpropsです。propsとは、親コンポーネントから子コンポーネントへデータを渡すための仕組みのことです。
ところが、アプリが大きくなってコンポーネントの入れ子が深くなると問題が起きます。たとえば、一番上の親コンポーネントにあるデータを、4階層や5階層下の子コンポーネントで使いたい場合、途中のコンポーネントがそのデータをまったく使わなくても、ただ下に渡すためだけにpropsを受け取って次へ渡し続けなければなりません。
この状態をProps drilling(プロップスドリリング)と呼びます。drillとは「穴を掘る」という意味で、まるで地面を深く掘り進めるようにpropsを渡し続けることからこの名前がついています。コードが複雑になり、修正や管理がとても大変になるため、Reactを学ぶうえで必ず理解しておくべき課題です。
2. Props drillingの問題をコードで確認してみよう
実際にProps drillingがどういう状態かを、シンプルなコードで確認してみましょう。親コンポーネントのユーザー名を、途中のコンポーネントを経由して孫コンポーネントまで渡す例です。
import React from "react";
function App() {
const userName = "山田さん";
return <Parent userName={userName} />;
}
function Parent({ userName }) {
// ParentはuserNameを使わないが、下に渡すだけのために受け取る
return <Child userName={userName} />;
}
function Child({ userName }) {
// ChildもuserNameを使わないが、下に渡すだけのために受け取る
return <GrandChild userName={userName} />;
}
function GrandChild({ userName }) {
// ようやくここで使う
return <p>ログイン中:{userName}</p>;
}
export default App;
このように、途中のコンポーネントが「ただの通り道」になってしまうのがProps drillingの問題です。コンポーネントの数が増えるほど、このような無駄な受け渡しが増えていきます。データの変更があったとき、途中のすべてのコンポーネントを修正しなければならないため、バグの原因にもなりやすいです。
3. Props drillingが引き起こす具体的なデメリット
Props drillingがなぜ問題視されるのか、具体的なデメリットを整理しておきましょう。
まず、コードの見通しが悪くなります。どのコンポーネントがどのデータを本当に必要としているのかが分かりにくくなります。ファイルを見ただけでは「このpropsはどこから来てどこへ行くのか」を追うだけで時間がかかります。
次に、修正コストが大きくなります。たとえばデータの名前を変更したい場合、途中のすべてのコンポーネントを修正する必要があります。変更漏れがあればエラーになるため、テストや確認の手間も増えます。
また、コンポーネントの再利用がしにくくなります。本来コンポーネントは独立した部品として別の場所でも使い回せるのが理想ですが、propsの受け渡しが前提になっているコンポーネントは他の場所で使いにくくなります。
これらの問題を根本から解決するのが、ReactのContext APIです。
4. Context APIがProps drillingを解決できる理由
Context APIは、コンポーネントの階層に関係なく、どこからでもデータを直接取り出せる仕組みを提供します。イメージとしては、建物の中に引かれた共有の水道管のようなものです。各部屋(コンポーネント)は、隣の部屋を経由しなくても、蛇口(useContext)をひねるだけで水(データ)を使えます。
Context APIを使うときの流れは次の3ステップです。1. createContextでContextを作る、2. Providerでデータを提供する範囲を囲む、3. useContextで必要なコンポーネントがデータを取り出す。この3ステップを押さえれば、Props drillingの問題をきれいに解消できます。
特に「アプリ全体で使うデータ」、たとえばログイン中のユーザー情報、テーマカラー、選択中の言語などは、Context APIで管理するのがとても向いています。
5. Context APIを使ってProps drillingを解消してみよう
先ほどのProps drillingのコードを、Context APIを使って書き直してみましょう。ParentとChildはuserNameを受け取る必要がなくなります。
import React, { createContext, useContext } from "react";
// Contextを作成する
const UserContext = createContext("");
function App() {
const userName = "山田さん";
return (
// Providerでアプリ全体を囲み、valueにデータをセット
<UserContext.Provider value={userName}>
<Parent />
</UserContext.Provider>
);
}
function Parent() {
// userNameは不要なのでpropsを受け取らなくてよい
return <Child />;
}
function Child() {
// こちらも同様
return <GrandChild />;
}
function GrandChild() {
// useContextで直接データを取り出す
const userName = useContext(UserContext);
return <p>ログイン中:{userName}</p>;
}
export default App;
コードがスッキリしたのが分かります。途中のコンポーネントはuserNameのことを知らなくてよくなり、それぞれが自分の役割だけに集中できるようになりました。これがContext APIの大きなメリットです。
6. Contextファイルを分けて管理する実践的な書き方
実際の開発では、Contextの定義を専用のファイルに切り出すのが一般的なやり方です。こうすることでコードが整理されて、チームでの開発や後からの修正もしやすくなります。ここではショッピングカートのアイテム数を管理する例で見てみましょう。
// CartContext.jsx
import { createContext, useState } from "react";
export const CartContext = createContext(null);
export function CartProvider({ children }) {
const [cartCount, setCartCount] = useState(0);
const addToCart = () => {
setCartCount((prev) => prev + 1);
};
return (
<CartContext.Provider value={{ cartCount, addToCart }}>
{children}
</CartContext.Provider>
);
}
次に、このProviderをAppで読み込み、カートのボタンコンポーネントでデータを使います。
// App.jsx
import React, { useContext } from "react";
import { CartProvider, CartContext } from "./CartContext";
function CartButton() {
const { cartCount, addToCart } = useContext(CartContext);
return (
<div>
<p>カートの中身:{cartCount}個</p>
<button onClick={addToCart}>カートに追加する</button>
</div>
);
}
function App() {
return (
<CartProvider>
<h1>オンラインショップ</h1>
<CartButton />
</CartProvider>
);
}
export default App;
このようにProviderコンポーネントを専用ファイルで作っておくと、アプリのどこからでもuseContextひとつで状態を読み書きできるようになります。
7. Props drillingとContext APIの使い分けポイント
Context APIはとても便利な機能ですが、すべての場面で使うのが正解というわけではありません。適切に使い分けることが、きれいなコードを書くうえで重要です。
まず、コンポーネントの階層が2〜3段程度であれば、通常のpropsで十分です。Contextを使うと逆にコードの読み方が複雑になることがあります。シンプルな親子関係のデータ受け渡しには、propsのほうが直感的で分かりやすいです。
一方で、次のようなケースではContext APIが向いています。アプリ全体のテーマ設定(ダークモード・ライトモードなど)、ログイン中のユーザー情報、多言語対応の言語設定、グローバルな通知やアラートの状態管理などが代表的な例です。
また、Contextの値が頻繁に変わるデータには注意が必要です。Contextの値が変わると、そのContextを参照しているすべてのコンポーネントが再描画されます。再描画(再レンダリング)とは、画面を更新するためにコンポーネントが再計算されることで、頻繁に起きるとアプリの動作が重くなる場合があります。1秒に何度も変わるようなデータはContextには向かないため、用途をしっかり選ぶことが大切です。
8. Props drillingを防ぐためのコンポーネント設計の考え方
Props drillingはContext APIで解決できますが、そもそもコンポーネントの設計を工夫することでも問題を起きにくくすることができます。
コンポーネントの分割粒度を見直すことが大切です。コンポーネントを細かく分けすぎると階層が深くなり、Props drillingが起きやすくなります。本当に分ける必要があるかを考えながら設計するとよいです。
また、データを使うコンポーネントのできるだけ近くで管理するという考え方も重要です。Reactには「データはそれを使うコンポーネントの近くに置く」という設計の原則があります。アプリ全体で使わないデータをわざわざ一番上の親コンポーネントで管理すると、不必要にpropsを渡す距離が長くなります。
Context APIとprops、どちらを使うかの判断基準は「どれだけ多くのコンポーネントで使うか」と「データはどれだけ変わるか」の2点を軸に考えると整理しやすいです。この考え方を身につけておくと、Reactのコード設計がぐっと上達します。