Menu

Reactのpropsのバケツリレーとは:原因と避け方

propsのバケツリレー(prop drilling)とは、下にあるコンポーネントのためだけに、それを使わないコンポーネントを通してpropを渡すことです。動く例で確かめ、アプリが大きくなると困る理由と、試すべき順番の解決策(コンポジション、コンテキスト、状態管理ライブラリ)を紹介します。

このページのコードはエディタで実行できます - 編集してすぐに結果を確認できます。

propsのバケツリレーとは、ツリーのさらに下にあるコンポーネントのためだけに、それを使わないコンポーネントを通してpropを渡すことです。ここでは user が App から Page と Sidebar を通って、それを読む唯一のコンポーネントである Avatar に届いています。

4階層あって、user を気にするのは最後の1つだけです。コンソールを見ると、Page と Sidebar が表示もしない値のために実行されていることがわかります。ボタンをクリックすると、またログを出します。

なぜ困るのか

2、3階層なら問題ありません。ツリーが大きくなると、propsのバケツリレーは問題になります。

  • 途中のコンポーネントが使わないpropsを運ぶ。Page と Sidebar のシグネチャに user propがあるので、それが何のためなのかを考えずには読めません。
  • 変更のたびにすべての層を触る。user.avatarUrl や onLogout のような2つ目の値を追加すると、両端の2つだけでなく、経路上のすべてのコンポーネントを編集することになります。
  • コンポーネントを移動すると鎖が切れる。Avatar を別の枝に移すと、新しい経路で user を通し直さなければなりません。
  • propを落としやすい。どこか1つの層で転送し忘れると、深いコンポーネントは undefined を受け取り、それを失った層ではエラーが出ません。

試してみてください。Avatar が呼ぶべき onLogout 関数を App に追加すると、4つのコンポーネントすべてを編集することになります。

解決策1:childrenを使ったコンポジション

これは見落とされがちな解決策です。深いコンポーネントがデータを使って描画できるようにデータを下へ渡すのではなく、データを持つコンポーネントが深いコンポーネントを自分で描画し、完成した要素を children(またはほかのprop)として下へ渡します。途中の層は枠を描画するだけで、user を目にすることはありません。

Page と Sidebar はもう、ユーザーの存在を知りません。App が user のある場所で Avatar を作り、完成した要素として下へ渡します。このようなレイアウトコンポーネントは再利用しやすくなり、onLogout の追加も App と Avatar だけの変更で済みます。それでも両方ともクリックのたびにログを出します。App がレンダリングのたびに新しい要素を渡すからです。コンポジションが簡単にするのはコードであって、レンダリングの回数ではありません。ほかの枠のパターンはchildrenのページで扱っています。

コンポジションが使えるのは、データを持つコンポーネントが深い部分の見た目も決められるときです。深いコンポーネントがライブラリの中や、持ち主が描画しない構造の中にある場合は役に立ちません。

解決策2:コンテキスト

さまざまな深さの多くのコンポーネントが同じ値(テーマ、ログイン中のユーザー、現在の言語)を必要とするなら、それをコンテキストに入れます。プロバイダーの下のどのコンポーネントも useContext で読め、途中のものは何も変わりません。

Avatar と Greeting は異なる2つの深さからユーザーを読み、Page と Sidebar はpropsを受け取りません。Page の下のどこに3つ目の読み手を追加しても、ほかに触れずに動きます。代償は、データの流れがpropsでは見えなくなることと、値が変わるとすべての読み手がレンダリングされることです。プロバイダー、デフォルト値、その再レンダリングについてはuseContextのページで扱っています。

解決策3:状態管理ライブラリ

多くのコンポーネントが読み、頻繁に変わる大きなアプリのstate(カート、ドキュメントエディタ、ライブデータ)には、Redux Toolkit、Zustand、Jotaiのような状態管理ライブラリを使うと、各コンポーネントが使うスライスだけを購読できます。これでバケツリレーも、1つの大きなコンテキストによる「すべての利用者がレンダリングされる」コストも避けられます。ただし依存関係と覚える概念が1つずつ増えるので、3階層を通るpropを直すためではなく、最初の2つの解決策では足りなくなったときに使ってください。

// Zustand, for comparison (not available in the editor here)
const useCart = create((set) => ({
    items: [],
    add: (item) => set((s) => ({ items: [...s.items, item] })),
}));

function CartCount() {
    const count = useCart((s) => s.items.length); // renders only when the count changes
    return <span>{count}</span>;
}

propsのバケツリレーで構わないとき

propsを渡すことはReactのデータの流れそのものであり、最も読みやすいパターンです。JSXを見るだけで、値を上から下まで追えます。その一部を使う1つか2つのコンポーネントを通るprop、あるいは密接に関係するコンポーネントの短い鎖を通るpropは、解決すべき問題ではありません。stateを最も近い共通の親に持ち上げて下へ渡し、途中の層が使い道のないpropsを運び始めたときにだけ方法を変えてください。

よくある質問

Reactのpropsのバケツリレーとは何ですか?

ツリーの深いところにあるコンポーネントに読ませるためだけに、それを使わない何層ものコンポーネントを通してpropを下へ渡すことです。途中の各コンポーネントがpropを受け取って転送します。

propsのバケツリレーは悪いことですか?

それ自体は悪くありません。値を2、3階層下へ渡すのは明示的で追いやすいものです。問題になるのは、多くの層が使わないpropsを運ぶようになり、名前の変更や新しいフィールドのたびに、関係のないファイルまで触ることになったときです。

propsのバケツリレーを避けるには?

まずコンポジションを試してください。親が深いコンポーネントを組み立て、children やほかのpropとして下へ渡せば、途中の層はデータを目にしません。さまざまな深さの多くのコンポーネントがその値を必要とするなら、コンテキストを使います。頻繁に変わる大きなstateなら、状態管理ライブラリを検討してください。

propsのバケツリレーの解決策はコンテキストだけですか?

いいえ。そして最善でないこともよくあります。children を使ったコンポジションなら、コンテキストを追加せずにバケツリレーをなくせ、データの流れもコード上で見えるままです。

Coddyのプログラミング言語のイラスト

Coddyでコードを学ぼう

始める