-
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 帳票 要件定義 設計 電力小売業界