プロップドリリングの呪い:なぜあなたのコードは「スパゲッティ」になるのか
やあ。今日もReactのコードベースと格闘している君へ。
Reactを書いていると、一度は必ず直面する壁がある。「propsを渡すためだけに、興味のないコンポーネントを何層も経由させる」という、あの無間地獄だ。現場ではこれを「プロップドリリング(Prop Drilling)」と呼ぶ。
これがなぜ厄介かと言えば、単にコードが汚れるからじゃない。コンポーネントの「責務」が曖昧になり、どこか一つでもpropsの型を変えようものなら、中間コンポーネントが次々と連鎖的に壊れる。まさに、修正するたびに胃が痛くなる負の遺産だ。
今日は、この泥沼から脱出し、中級から一歩上のアーキテクトへと進化するための「武器」を整理しよう。
—
プロップドリリングが起きる真の理由
Reactは単方向データフローだ。親から子へ、データは滝のように流れる。しかし、データが「孫の孫」に必要になったとき、私たちはつい中間コンポーネントにそのデータを突き刺してしまう。
ブラウザの裏側、つまりReactのレンダリングプロセスに視点を移そう。Reactは仮想DOMを比較し、差分を反映させるが、プロップドリリングが起きていると、不必要なコンポーネントまで再レンダリングの対象候補に入ってしまうことがある。React.memoで防ぐという手もあるが、そもそも「不必要なデータを流している」という設計自体が、パフォーマンスチューニング以前の問題なんだ。
—
解決策1:Context APIという「近道」
最も手軽で標準的な解決策は `Context API` だ。これは「Propsのバケツリレーをスキップするトンネル」だと考えてほしい。
import React, { createContext, useContext } from ‘react’;
// 1. コンテキストを作成(型安全を意識してundefinedを許容するか初期値を設定)
const UserContext = createContext<{ name: string } | null>(null);
// 2. Providerでアプリケーションを包む
export const AppProvider = ({ children }: { children: React.ReactNode }) => {
return (
{children}
);
};
// 3. 使うときはカスタムフックにして切り出すのがプロの作法
export const useUser = () => {
const context = useContext(UserContext);
if (!context) throw new Error(‘useUserはProvider内で使ってください’);
return context;
};
// 4. 深い階層のコンポーネント:Propsを一切受け取らずにデータにアクセス
const Profile = () => {
const { name } = useUser();
return
;
};
シニアの視点:
Contextは非常に強力だが、「頻繁に更新される値」をContextに入れるのはNGだ。ReactのContextは、値が変わるとProvider以下の全てのConsumerが再レンダリングされる。これが大規模アプリで起こると、アプリ全体が重くなる原因になる。Contextは「ログインユーザー情報」や「テーマ設定」のような、更新頻度の低いグローバルな設定に適している。
—
解決策2:コンポーネントの分割と「コンポジション」
意外と忘れられがちなのが、これ。「データが必要なコンポーネントを、あえて外側で組み立てる」という手法だ。
// 悪い例:Propsをバケツリレーしている
const Page = ({ user }) =>
const Header = ({ user }) =>
// 良い例:コンポジションで解決する
const Page = ({ children }) => (
);
// 使う側で組み立てる
コンポーネントを「データを受け取る容器」ではなく「見た目を組み立てるパーツ」として再定義するだけで、プロップドリリングは劇的に減る。
—
解決策3:状態管理ライブラリ(Zustandなど)
もし、アプリケーションが中規模を超え、複雑な状態(サーバーキャッシュや非同期データ)が絡むなら、迷わず `Zustand` を導入しよう。Reduxのような巨大なボイラープレートはもう古い。
import { create } from ‘zustand’;
// ストアの作成
const useStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}));
// どこからでも直接アクセス可能
const Counter = () => {
const { count, increment } = useStore();
return ;
};
Zustandの素晴らしい点は、「必要なステートだけをサブスクライブできる」ことだ。コンポーネントが必要なデータが変わったときだけ再レンダリングされるため、パフォーマンスも非常に優秀。Redux時代に苦労した「Providerのネスト地獄」もこれで解消だ。
—
最後に:アーキテクトからのアドバイス
いいかい、「何が何でもライブラリを使う」のが正解じゃない。
1. まずはコンポジション(構成)で解決できないか考える。
2. 次にContext APIで十分か検討する。
3. それでも管理が辛い、またはパフォーマンスがボトルネックならZustand等の状態管理を入れる。
この順序を忘れないでほしい。技術は目的ではなく手段だ。コードを簡潔に保ち、自分以外のメンバーが「なぜこう書いたのか」を一目で理解できること。それが、真に優れたフロントエンドエンジニアの仕事だよ。
さあ、エディタに戻って、その汚れたpropsのバケツリレーをリファクタリングしてみよう。きっと、驚くほど視界が晴れるはずだ。応援しているよ。

コメント