Prop Drilling問題(バケツリレー問題)とは?
Prop Drillingは、Reactのコンポーネント構造において、データ(Props)を親コンポーネントからその子孫コンポーネントへと階層を下っていく形で渡していくプロセスのことです。
Reactアプリケーションは多くのコンポーネントで構成され、これらはツリーのような階層構造を形成しています。Reactには、アプリケーションの状態管理を単純化し、データの流れを追跡しやすくするため「データは常に親から子へと一方向に流れる」という基本原則があります。そのため、複数の階層を持つコンポーネントツリーにおいて、データを直接下位のコンポーネントに渡す(間を飛ばす)ことは基本的にはできません。
Prop Drillingの問題は、各中間コンポーネントがデータを直接使用しなくても、それを下位のコンポーネントに渡すために必要になってしまうことです。
これは特に多くの階層を持つ大規模なアプリケーションでややこしくなりがちです。途中の中間コンポーネントは実際には、そのデータ(Props)を必要としないにも関わらず、データを「中継」する役割を担う必要があるため、コードの複雑化や再利用性の低下に繋がりやすくなります。
引用記事においては以下の具体的方法を紹介しています。高階コンポーネントは高階関数のアナロジーで非常に興味深い手法でした。
- 方法①:Context API
- 方法②:コンポーネント合成 (Composition)
- 方法③:高階コンポーネント(HOC)
方法論より先にこの問題がなぜ起こるのか考える
それはさておき、解決策に飛びつくより問題が生じている理由をメタに考えてみます
途中の中間コンポーネントは実際には、そのデータ(Props)を必要としないにも関わらず、データを「中継」する役割を担う必要があるため
と解説されていますが本当にただ中継するだけならば親コンポーネントの直下に下位コンポーネントを入れて中間コンポーネントを削除すればいいはずでしょう。そうしないのは中間コンポーネントがデータと関係ない責任を負っているからです。
中間コンポーネントの責任は下位コンポーネントの引数(Props)を与えることではなく下位コンポーネントの配置やレイアウトを決定することの場合が多いと考えています。
どこに下位コンポーネントを置くかだけに関心を持つコンポーネントの書き方について明確な答えが無い、それがdrilling問題の生じる理由です。
下位コンポーネントを置くかだけに関心を持つコンポーネントの書き方の一つは引用記事にもあるCompositionです。ですがいくつかデメリットがあります。
- JSX風の宣言的な書き方を崩してしまうこと
- 固定長のコンポーネントしか扱えないこと
JSX風の宣言的な書き方を崩してしまうというのは、それを良しとするならデメリットになりえませんが。<親><子供/></親>
のように子供は親の内側というXML的な書き方を崩して<親 child=<子供/> />
のように書くのは視認性が良くないです(私にとって)。
固定長のコンポーネントしか扱えないというのは、例えばリスト、タグ、タブの各アイテムの周囲を装飾する中間コンポーネントを作れないということです。各要素に同じ装飾するだけなら別のコンポーネントで包めばいいのですが、例えば選択されたタブに対応する子要素だけ表示したい場合など、ロジックの結果によって装飾を変化させたい場合などに対応できません。
提案方法:childrenのkeyで下位コンポーネントの配置を制御する
コンポーネントはprops.childrenのkeyを読み出すことが可能です。これを利用して下位コンポーネントの配置を制御しようというのがこの記事の提案です。
ページ上部のNavの例を示します。
下位コンポーネント(a,Logo,Avatar)の配置にのみ責任を持ちpropsは受け取らないNavの例
import React from 'react';
const Avatar=(props:{name: string, src: string})=>{
return <>
<img src={props.src} height={"20px"} width={"auto"} alt={"logo"}/>
<span color={"white"}>{props.name}</span>
</>
}
const Logo=()=>{
return <span style={{fontSize:20, color:"white"}}>Logo</span>
}
const Nav=(props:{children:React.ReactElement|React.ReactElement[]})=>{
const children=Array.isArray(props.children)?props.children:[props.children]
return <div style={{backgroundColor:"#8888ff", padding:"10px", minWidth: 600}}>
{children.filter(v=>v.key?.includes("logo"))}
{children.filter(v=>v.key?.includes("item")).map(v=><span key={v.key} style={{padding:"0 10px 0 10px"}}>{v}</span>)}
<div style={{float:"right"}}>
{children.filter(v=>v.key?.includes("avatar"))}
</div>
</div>
}
export function App(props) {
return (
<Nav>
<Avatar key="avatar" name={"佐藤"} src={"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAABBSURBVChTY/wPBAwwsP4UhA40g9BAwASlEZIggMRGKMABEAqQjEVmo7oBC0AoQHYDCEBNgViBLgkCUDECjmRgAADc8hb0URqaYAAAAABJRU5ErkJggg=="}/>
<Logo key="logo"/>
<a key="item_home" href={"/home"}>Home</a>
<a key="item_product" href={"/product"}>Product</a>
<a key="item_about" href={"/about"}>About us</a>
</Nav>
);
}
Navコンポーネントはkeyにlogoが含まれている子供は左端、keyにavatarが含まれている子供は右端、keyにitemが含まれている子供は中間に並べています。Navコンポーネントは何のpropsも受け取らずchildrenをどこに置くのか、どう装飾するのかという仕事に集中しています。責任の切り分けができている状態です。
Compositionの問題点と比較すると、JSX風に下位コンポーネントは内側に書くという<親><子供/></親>
の原則も守られています。可変長の<a></a>
を装飾するCompositionでは扱いずらい問題も解決しています。
あえて問題点を挙げるとすればkeyの命名規則がNav(中間コンポーネント)の上位に伝わらないことでしょう。Compositionならばpropsのフィールド名から動作が予想できますが、この方法ではコメントに書かないと伝わらないでしょう。チームで開発している際に本手法を採用するときには吟味が必要だと思います。
最後に
貴方の御用のためにお使いください