React Context を使ってProp drilling問題を解消

React Context を使ってProp drilling問題を解消

なにを解決するのか

Contextを使用するとツリーの各階層で親→子→孫のように明示的に値を渡すことなく、コンポーネント間で値を共有する事が可能になります。

Contextはグローバルライクに使用したいpropsがある場合に利用されます。
グローバルライクと言っても、どこからでもアクセス可能という訳でなく明示的に指定した範囲内でのみ参照が可能になります。

Contextを使用しない例

例として、X Y Zコンポーネントがあり、Xの子コンポーネントにY、Yの子コンポーネントにZコンポーネントがあるとします。

Xコンポーネントはtodoという状態を持っており、Zコンポーネント内にてこの値を使用します。

Reactではpropsを用いて上位コンポーネントから下位コンポーネントに値を渡します。

Xコンポーネントが持つtodoという値をZコンポーネントで使用する場合はYを経由して値を伝播させる必要性があります。

YコンポーネントはZコンポーネントをレンダリングさせるだけなので、todoという値が必要ありません。

今回の例で言えば、伝播させる回数は1回のみであるので特に問題はありませんが、これが何層にもネストしていくとかなり面倒という事が想像出来ます。

これはProp drilling問題(バケツリレーとも言われる)と言われており、解決法としてはReduxもしくはContextを使用する事です。

// Zコンポーネント内 
// Yコンポーネントから伝搬された値を表示させる

const Z = ({ todo }) => {

  return <p>{todo}</p>

}

// Yコンポーネントは親コンポーネントであるXからtodoという値を受け取りZコンポーネントに伝播しているだけ

const Y = ({ todo }) => {

  return  <Z todo={todo} />

}

// X → Y → Zの順番でバケツリレーを行っている

const X = () => {

  const [todo, setTodo] = React.useState('hello world');

  return <Y todo={todo} />;

};

ReactDOM.render(

  <X />,

  document.getElementById('root')

)

Contextを使用する

Contextは導入が非常に楽であり、必要なステップはたったの3つだけです。

  1. Contextを作成する
  2. Providerに共有させたい値をセットする
  3. Consumerを定義して値を受け取る

下記コードのようにContextを利用すれば伝播作業が不要になります。(YコンポーネントはZコンポーネントをレンダリングするのみ)

const Context = React.createContext();

const Z = () => {
  return (
    <Context.Consumer>
      {value => {
        return <p>{value}</p>
      }}
    </Context.Consumer>
  )
}

const Y = () => {
  return  <Z />
}

const X = () => {
  const [todo, setTodo] = React.useState('hello world');
  return (
    <Context.Provider value={todo} >
      <input value={todo} onChange={e => setTodo(e.target.value)} />
      <Y />
    </Context.Provider>
  )
};

ReactDOM.render(
  <X />,
  document.getElementById('root')
)

1. Contextを作成する

React.createContext()でContextを作成します。
Contextの名前は任意です。

React.createContext()で作成されたコンテクストオブジェクトはProvider, Consumerメソッドを持っています。

2. Providerに共有させたい値をセットする

<Context.Provider value={ } >のvalueプロパティには共有させたい値をセットします。

// 今回参照させたい値は `todo` なのでProviderコンポーネントのvalueプロパティに todoを渡している。
// この実装によりY, Zコンポーネント内で `todo` という値が参照可能になる

const X = () => {

  const [todo, setTodo] = React.useState('hello world');

  return (

    <Context.Provider value={todo} >

      <input value={todo} onChange={e => setTodo(e.target.value)} />
      <Y />

    </Context.Provider>

  )
};

<Context.Provider >でラップされたコンポーネント(今回はYとZコンポーネント)内で<Context.Consumer>コンポーネントを定義するとProviderで渡されたvalueを参照する事が可能になります。

3. Consumerを定義して値を受け取る

<Context.Consumer>コンポーネントを配置します。

ラップされている内部の関数のvalue引数にはProviderでセットした値が格納されています。

const Z = () => {

  return (


    <Context.Consumer>

      {value => {

        return <p>{value}</p>

      }}

    </Context.Consumer>

  )
}

useContextを使う

先ほどのステップ3Consumerを使用してコンテキストの値を取り出す実装をしましたが、
現在はuseContextという機能がReactに追加されより簡単に値を取り出す事が可能です。

const Z = () => {

  // useContext関数の引数にContextを渡すだけで値を取得できる。
  const value = React.useContext(Context)

  return <p>{value}</p>
     
}