-
React のHOC(高次コンポーネント)の使い所 1:Props Proxy
- 2018年10月15日
- JavaScript
- React
React の実装方法の 1 つに HOC(High Order Component: 高次コンポーネント)という方法があります。
HOC は React の公式ドキュメントにも 紹介されており、冗長なコードをなくし効率よく開発を進められる方法です。 ですが日本語で解説された記事があまり見つからず、馴染みの薄い方も多いのではないでしょうか。
この記事では実装手法別に 2 回に分けて、HOC の使い方を解説します。
HOC とはなんぞや
HOC は High Order Component の略で、「高次コンポーネント」と訳されます。
一般的に HOC は コンポーネントを受け取り、コンポーネントを生成する関数 のことを指します。
// コンポーネントを引数にとり、より高次的なコンポーネントを返却する
const EnhancedComponent = higherOrderComponent(WrappedComponent);
HOC のには主に2つの実装方法があります。
- Props Proxy (Props のプロキシ)
- Inheritance Inversion (継承の逆転)
の2 つです。まずこの記事では、Props Proxy の実装について紹介します。
公式ドキュメント Higher-Order Components に記載されているサンプルは Props Proxy での実装です。 公式ドキュメントのサンプルでは、ブログの投稿(BlogPost)とそのコメント(CommentList)について書かれていますが、 ここでは Trello のようなかんばんアプリを考え、公式ドキュメントのサンプルコードに少し手を加えて紹介します。
素直なかんばんアプリの実装
特に HOC を使わずにかんばんアプリを実装した例です。
このコードでは、 Board
コンポーネントが List
コンポーネントをラップし、 List
コンポーネントが Card
コンポーネントをラップしています。
この Board
コンポーネントと List
コンポーネントには以下のような共通点があります。
componentDidMount
,componentWillUnmount
でイベントリスナhandleChange
の追加・削除を行っている- イベントリスナ
handleChange
はDataSource
の更新を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
は、ラップするコンポーネント WrappedComponent
の data
へ渡されています(data={this.state.data}
の部分です)。
<WrappedComponent data={this.state.data} {...this.props} />
1 点ここで大事なことは、 {...this.props}
の記述です3。 この記述が意味するのは、ラップ元コンポーネント(親コンポーネント)へ渡された props
は、 ラップされるコンポーネント(子コンポーネント)の props
へそのまま渡されるということです4。 この記述がない場合、ラップされたコンポーネントは data
以外の props
を受け取れなくなってしまいます。
(例えば上記のサンプルコードでは、 List
コンポーネントが list
を受け取れなくなってしまいます)
さて、データ取得部分のロジックに関しては withSubscription
とその引数に渡す関数で考慮するので、 List
コンポーネント自体は、 data
に Card
のデータ部分の配列が渡されることを意識して実装すればよいということになります。
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
コンポーネントでは data
に List
のデータ部分の配列が渡されるので、
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 のサンプルはこちらにまとめています。
DataSource
のもつデータが更新された際、addChangeListener
で事前にイベント登録されたメソッド(この例ではBoard
とList
コンポーネントがもつそれぞれのhandleChange
メソッド)を呼び出します。本稿では主題とずれるため掘り下げませんが、詳しく知らない方は EventEmitter について調べてみてください。↩HOC の再利用性の高さがこのかんばんアプリでも機能していることがわかります。↩
親コンポーネントの
props
がそのまま子コンポーネントへ渡されることが「Props Proxy」と呼ばれるゆえんです。↩List
やBoard
がstate
を持たない「Stateless functional components」になっています。↩React-Redux を扱ったことがあれば、これら 2 つのコンポーネントの関係が、Container コンポーネントと Presentational コンポーネントの関係に似ていることに気づくかも知れません。↩
この記事を書いた人 : 上田直諒
AWS bluebird css CSV docker docker compose electron ES6 es2015 Git Heroku ITコンサルティング JavaScript justinmind less mongoDB Node.js php PostgreSQL Private Space Promise React react-router reactjs Salesforce scss Selenium Builder selenium IDE Selenium WebDriver stylus TypeScript VirtualBox VisualStudioCode vue vuejs webpack システム開発プロジェクト セキュリティ ワイヤーフレーム 上流工程 卒FIT 帳票 要件定義 設計 電力小売業界