😸

Prop Drilling

2023/02/07に公開

この記事のゴールは prop drilling('threading'とも呼ばれる)とは何かを理解してもらうだけでなく、prop drillingがどのような場合に問題になるのか、そして問題を回避するための仕組みを知ってもらう事です。

prop drilling とは?

prop drilling(またはthreading)はReactコンポーネントツリーの一部へデータを渡す処理の事です。ステートフルなコンポーネントの非常に単純な例を見てみましょう(これは、私のお気に入りの例です)。

function Toggle() {
  const [on, setOn] = React.useState(false)
  const toggle = () => setOn(o => !o)
  return (
    <div>
      <div>The button is {on ? 'on' : 'off'}</div>
      <button onClick={toggle}>Toggle</button>
    </div>
  )
}

では、これを2つのコンポーネントにリファクタリングしてみましょう。

function Toggle() {
  const [on, setOn] = React.useState(false)
  const toggle = () => setOn(o => !o)
  return <Switch on={on} onToggle={toggle} />
}

function Switch({on, onToggle}) {
  return (
    <div>
      <div>The button is {on ? 'on' : 'off'}</div>
      <button onClick={onToggle}>Toggle</button>
    </div>
  )
}

Switchコンポネートはtoggleonステートへの参照を必要とするので、propsとして渡しています。もう1つコンポーネントツリーに別レイヤーを追加するリファクタリングをしてみましょう。

function Toggle() {
  const [on, setOn] = React.useState(false)
  const toggle = () => setOn(o => !o)
  return <Switch on={on} onToggle={toggle} />
}

function Switch({on, onToggle}) {
  return (
    <div>
      <SwitchMessage on={on} />
      <SwitchButton onToggle={onToggle} />
    </div>
  )
}

function SwitchMessage({on}) {
  return <div>The button is {on ? 'on' : 'off'}</div>
}

function SwitchButton({onToggle}) {
  return <button onClick={onToggle}>Toggle</button>
}

これがprop drillingです。onステートと toggleハンドラーを取得するために、Switchコンポーネントを介してpropsを渡す必要があります。Switchコンポーネントは実際にonステートとtoggleハンドラーを必要としませんが、子コンポーネントがそれらを必要としているため、propsを受け取り、渡さなければいけません。

prop drillingはなぜ良いのか?

グローバル変数を使用するアプリケーションで仕事をしたことがありますか? 非分離型の$scope継承(もしくは恐ろしい$rootScope)を活用したAngularJS。これをコミュニティがほとんど拒絶している理由は、アプリケーションのデータモデルが非常に分かりにくくなることに繋がるからです。どこでデータが初期化され、どこで更新され、どこで使用されているのかを見つ出す事が誰にとっても困難になるからです。「何も壊さずにコードを修正/削除することができるか?」といる問いに答えることは、そのような状況では困難なのです。そして、この質問こそあなたがコードを最適化すべき時に必須の質問です。

グローバル変数よりもESModuleを好む理由の1つは、値がどこで使われるかをより明確にする事ができ、値の追跡がより簡単になり、そして、あなたの変更がアプリケーションの残りの部分にどのような影響を与えるかを把握し易いからです。

prop drillingの最も基本は、単にアプリケーションのビュー全体を通して明示的に値を渡すことです。これは素晴らしいことで、Toggleコンポーネントにてonステートをenumにリファクタリングしたいなら、コードを追うこと(実行しなくても)で使われている全ての場所を簡単に追跡することができ、変更することができます。ここで重要なのは暗黙的ではなく、明示的だということです。

prop drillingはどのような問題を引き起こすのか?

先程の例では全く問題ありません。しかし、アプリケーションが成長するにつれ、何層ものコンポーネントを使用するかもしれません。最初に書き出した時は通常大したことはないのですが、そのコードが数週間も作業された後、いくつかのユースケースで扱いにくくなってきます。

  • データをリファクタリングする(ex: {user: {name: 'Joe West'}}{user: {firstName: 'Joe', lastName: 'West'}})
  • 特定のpropsを必要とするコンポーネントの(再)移動による、Over-forwarding(必要以上のpropsを渡すこと)
  • Under-forwarding とdefaultPropsの乱用により、(コンポーネントの移動による)propsの欠落に気付かない
  • 途中でpropsをリネームすると、それを脳内で把握するのは難しい(例えば、Toggle on={on} /><Switch toggleIsOn={on} />をレンダリングする)。

他にも様々なシチュエーションがあり、特にリファクタリングの過程で、prop drillingは苦痛の原因となります。

prop drillingの問題をどうやって避けるか?

prop drillingで問題を悪化させることの1つに、render内を不必要な複数のコンポーネントに分割することです。大きなrenderメソッドは、できるだけインライン化することで驚くほどシンプルになります。早々に分割する必要はありません。ブロックを再利用する必要が本当にあるまで、そのブロックを分割するのを待ちましょう。そうすれば、propsをどこにも渡す必要がありません。

面白いことに、アプリケーション全体を単一のReactコンポーネントとして書くことを技術的に止めるものは何もありません。アプリケーション全体のstateを管理することができ、巨大なrenderメソッド1つで済みます...。これを推奨しているわけはありません。ただ、考えるべきことはあります。

prop drillingの影響を軽減するためにできるもう1つのことは、必須のpropsにdefaultPropsを使用するのを避けることです。コンポーネントが適切に機能するために必須なpropsにdefaultPropsを使用するのは、重要なエラーを隠蔽し、無言で失敗させるだけです。ですので、オプショナルなpropsにのみdefaultPropsを使いましょう。

stateをできるだけ関連性のある場所に配置します。もしアプリケーションの一部がstateを必要とするなら、そのstateをアプリの最上位に配置するよりも、それらのコンポーネントの共通の親コンポーネントで管理します。
state管理については私のブログを参照してください。Application State Management

Reactツリーの深い場所で本当に必要なものにContext APIを使って下さい。Context APIはアプリケーション内のあらゆる場所で必要のもではありません(プロバイダはアプリケーションのどこにでも配置できます)。これはprop drillingに関するいくつかの問題を回避する手助けとなります。Context APIはグローバル変数の時代に逆戻りするものだと指摘されています。違いはAPIが設計されているので、ccontextのソースやコンシューマを比較的簡単に見つけることができることです。

結論

prop drillingは良いこともあれば、悪いこともあります。上で述べたいくつかのグットプラクティスに従えば、よりメンテし易くする為の機能として使うことができます。Good luck!

Discussion