propsのバケツリレーとは、ツリーのさらに下にあるコンポーネントのためだけに、それを使わないコンポーネントを通してpropを渡すことです。ここでは user が App から Page と Sidebar を通って、それを読む唯一のコンポーネントである Avatar に届いています。
4階層あって、user を気にするのは最後の1つだけです。コンソールを見ると、Page と Sidebar が表示もしない値のために実行されていることがわかります。ボタンをクリックすると、またログを出します。
なぜ困るのか
2、3階層なら問題ありません。ツリーが大きくなると、propsのバケツリレーは問題になります。
- 途中のコンポーネントが使わないpropsを運ぶ。
PageとSidebarのシグネチャにuserpropがあるので、それが何のためなのかを考えずには読めません。 - 変更のたびにすべての層を触る。
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 を使ったコンポジションなら、コンテキストを追加せずにバケツリレーをなくせ、データの流れもコード上で見えるままです。