React のHOC(高次コンポーネント)の使い所 1:Props Proxy


React の実装方法の 1 つに HOC(High Order Component: 高次コンポーネント)という方法があります。

HOC は React の公式ドキュメントにも 紹介されており、冗長なコードをなくし効率よく開発を進められる方法です。 ですが日本語で解説された記事があまり見つからず、馴染みの薄い方も多いのではないでしょうか。

この記事では実装手法別に 2 回に分けて、HOC の使い方を解説します。

HOC とはなんぞや

HOC は High Order Component の略で、「高次コンポーネント」と訳されます。

一般的に HOC は コンポーネントを受け取り、コンポーネントを生成する関数 のことを指します。

HOC のには主に2つの実装方法があります。

  • Props Proxy (Props のプロキシ)
  • Inheritance Inversion (継承の逆転)

の2 つです。まずこの記事では、Props Proxy の実装について紹介します。

公式ドキュメント Higher-Order Components に記載されているサンプルは Props Proxy での実装です。 公式ドキュメントのサンプルでは、ブログの投稿(BlogPost)とそのコメント(CommentList)について書かれていますが、 ここでは Trello のようなかんばんアプリを考え、公式ドキュメントのサンプルコードに少し手を加えて紹介します。

素直なかんばんアプリの実装

特に HOC を使わずにかんばんアプリを実装した例です。

CodePen実際に動くコードを置いておきました。

このコードでは、 Board コンポーネントが List コンポーネントをラップし、 List コンポーネントが Card コンポーネントをラップしています。

この Board コンポーネントと List コンポーネントには以下のような共通点があります。

  • componentDidMount , componentWillUnmount でイベントリスナ handleChange の追加・削除を行っている
  • イベントリスナ handleChangeDataSource の更新を setState によりコンポーネントに反映している1

この規模のコードであればこのままでもよさそうですが、アプリケーションの規模がある程度大きくなるような場合、 共通化できる処理はコンパクトにまとめたいものです。

Props Proxy による実装

同様のコードを Props Proxy による HOC で実装してみます。

実際に動くコードはこちらです。

HOC として機能しているのは withSubscription です。(これは、 Reactのドキュメントページ に書かれたコードと同じものです2

先程の共通なロジック部分が withSubscription 内に記述されています。

withSubscription は、引数にラップするコンポーネント WrappedComponent と、 DataSource からデータを取得する関数 selectData を受け取ります。

selectData によって DataSource から取得したデータを state に保持していますが、

これは ListWithSubscription では Card のデータ部分の配列が、 BoardWithSubscription では List のデータ部分の配列が、それぞれ state に保持されるということです。

ListWithSubscription = withSubscription(
  List,
  (DataSource, props) => DataSource.getCards(props.list.id) // -> Card のための配列
)

BoardWithSubscription = withSubscription(
  Board,
  (DataSource) => DataSource.getLists() // -> List のための配列
)

また、ラップ元が state で保持したプロパティ data は、ラップするコンポーネント WrappedComponentdata へ渡されています(data={this.state.data} の部分です)。

<WrappedComponent data={this.state.data} {...this.props} />

1 点ここで大事なことは、 {...this.props} の記述です3。 この記述が意味するのは、ラップ元コンポーネント(親コンポーネント)へ渡された props は、 ラップされるコンポーネント(子コンポーネント)の props へそのまま渡されるということです4。 この記述がない場合、ラップされたコンポーネントは data 以外の props を受け取れなくなってしまいます。

(例えば上記のサンプルコードでは、 List コンポーネントが list を受け取れなくなってしまいます)

さて、データ取得部分のロジックに関しては withSubscription とその引数に渡す関数で考慮するので、 List コンポーネント自体は、 dataCard のデータ部分の配列が渡されることを意識して実装すればよいということになります。

const List = ({ list, data }) => {
  return (
    <div className="column"
      style={{ backgroundColor: list.color }}>
      <header>{`${list.name} (id:${list.id})`}</header>
      {data.map((card, idx) => // data は Card のデータ部分に相当する配列
         <Card card={card} key={idx} />
       )}
    </div>
  )
}

同様に Board コンポーネントでは dataList のデータ部分の配列が渡されるので、


const Board = ({ data }) => {
  return (
    <div className="columns">
      {data.map(list => // data は List のデータ部分に相当する配列
         <ListWithSubscription list={list} key={list.id} /> // List を直接使っていないことに注意
      )}
    </div>
  )
}

という記述だけで済むことになります。

Props Proxy の勘所

Props Proxy は、ラップするコンポーネントの props をある程度操作できることが大きなメリットになります。

この記事の例では、親コンポーネントで抽出したデータを、子コンポーネントへ props 経由で流し込むことで、 ロジックと描画の部分をうまく切り分けることに成功しています56

他にも、

  • 親コンポーネントの state を更新するためのハンドラを、子コンポーネントへ渡す
  • スタイリングを行う別のコンポーネントでラップする
  • 別の props を追加してコンポーネントを拡張する

といった用途で使うことができます。

注意としては、処理とは関係のない props を改変したり削除しないよう、プロパティ名を適切に扱うようにしましょう。

他参考


CodePen のサンプルはこちらにまとめています。


  1. DataSource のもつデータが更新された際、 addChangeListener で事前にイベント登録されたメソッド(この例では BoardList コンポーネントがもつそれぞれの handleChange メソッド)を呼び出します。本稿では主題とずれるため掘り下げませんが、詳しく知らない方は EventEmitter について調べてみてください。

  2. HOC の再利用性の高さがこのかんばんアプリでも機能していることがわかります。

  3. スプレッド構文について詳しく知らない方はこちらも参考にしてください

  4. 親コンポーネントの props がそのまま子コンポーネントへ渡されることが「Props Proxy」と呼ばれるゆえんです。

  5. ListBoardstate を持たない「Stateless functional components」になっています。

  6. React-Redux を扱ったことがあれば、これら 2 つのコンポーネントの関係が、Container コンポーネントと Presentational コンポーネントの関係に似ていることに気づくかも知れません。