Higher-Order Components(以下、HOC)は、Reactのコンポーネントを作る際のパターン。
HOCを使うことで、複数のコンポーネントで使っている処理を共通化したり、SFCにライフサイクルメソッドを追加したりすることが出来る。
基本的な構造
HOCは、以下のような関数を使って実現する。
function hocFactory(WrappedComponent) { return class extends React.Component { render() { return <WrappedComponent />; } }; }
コンポーネントを引数として受け取り、それに機能を追加した新しいコンポーネントを返す。
上記の例では何もしていないが、hocFactory
(ファクトリ関数)のなかで様々な処理を行うことで、WrappedComponent
に新しい機能を加えることが出来る。
このエントリでは、HOCを使った以下のテクニックについて説明する。
- propsをファクトリ関数から受け取る
- SFCにライフサイクルメソッドを追加する
propsをファクトリ関数から受け取る
propsとしてtext
を受け取りそれを表示する以下のようなコンポーネントがあるとする。
function Basic({text}) { return <div>{text}</div>; }
<Basic text="abc" />
とすれば、abc
と表示される。
同じことをHOCでやる場合、次のようになる。
function hocFactory(WrappedComponent) { return class extends React.Component { render() { return <WrappedComponent text="abc" />; } }; } const EnhancedBasic = hocFactory(Basic);
こうすると、<Basic text="abc" />
と<EnhancedBasic />
は同じ内容になり、どちらもabc
と表示される。
もちろんこの例の場合、HOCを使う意味はないし、むしろ煩雑になっているだけである。
だが異なるコンポーネントに共通の機能を渡したいケースなどでは、このテクニックが役に立つ。
インライン要素のテキストを表示するTextContent
と、リストを表示するListContent
という2つのコンポーネントを用意して、それを使いながら説明してく。
HOCを使わない場合
TextContent
とListContent
の内容は次の通り。
function TextContent({text}) { return ( <span> {text} </span> ); } function ListContent({contents}) { return ( <ul> {contents.map(i => <li key={i}>{i}</li>)} </ul> ); }
以下のようにすることで、それぞれテキストとリストが表示される。
<TextContent text="xyz" /> <ListContent contents={['abc', '123', 'def']} />
このコンポーネントに、以下の機能を追加することになった。
- 要素をクリックすることで、文字の色が変わる
TextContent
は、黒⇔赤ListContent
は、黒⇔オレンジ
以下が、これを実装したもの。
class TextContent extends React.Component { constructor(props) { super(props); this.state = {color: 'black'}; this.changeColor = this.changeColor.bind(this); } changeColor() { this.setState({color: this.state.color === 'black' ? 'red' : 'black'}); } render() { const {color} = this.state; return ( <span style={{color}} onClick={this.changeColor}> {this.props.text} </span> ); } } class ListContent extends React.Component { constructor(props) { super(props); this.state = {color: 'black'}; this.changeColor = this.changeColor.bind(this); } changeColor() { this.setState({color: this.state.color === 'black' ? 'orange' : 'black'}); } render() { const {color} = this.state; return ( <ul style={{color}} onClick={this.changeColor}> {this.props.contents.map(i => <li key={i}>{i}</li>)} </ul> ); } }
機能としては問題ないが、一目見て分かる通り、ほとんど同じ内容が重複して記述してある。
HOCを使うことで、効率よく記述することが可能になる。
HOCを使って書き直す
まず、TextContent
とListContent
をシンプルな形に戻す。
そして、文字の色はprops.color
として受け取り、クリックによる文字色の変更機能はprops.onClick
として受け取るようにした。
function TextContent({text, color, onClick}) { return ( <span style={{color}} onClick={onClick}> {text} </span> ); } function ListContent({contents, color, onClick}) { return ( <ul style={{color}} onClick={onClick}> {contents.map(i => <li key={i}>{i}</li>)} </ul> ); }
<Basic />
の例で示したように、HOCのファクトリ関数はpropsをコンポーネントに渡すことが出来るのだから、それを利用すればいい。
以下が、HOCを使って実装したバージョン。
共通の処理をファクトリ関数にまとめている。
function hocFactory(WrappedComponent, color1, color2) { return class extends React.Component { constructor(props) { super(props); this.state = {color: color1}; this.changeColor = this.changeColor.bind(this); } changeColor() { this.setState({color: this.state.color === color1 ? color2 : color1}); } render() { return ( <WrappedComponent {...this.props} color={this.state.color} onClick={this.changeColor} /> ); } }; } const EnhancedTextContent = hocFactory(TextContent, 'black', 'red'); const EnhancedListContent = hocFactory(ListContent, 'black', 'orange');
<EnhancedTextContent text="xyz" /> <EnhancedListContent contents={['abc', '123', 'def']} />
注意点としては、以下の{...this.props}
を忘れないこと。
これを記述しないと、新しく作られたコンポーネントに渡されたprops(この例ではtext
やcontents
)が反映されない。
<WrappedComponent {...this.props} color={this.state.color} onClick={this.changeColor} />
また、{...this.props}
のあとにpropを定義すると、その内容で上書きされる。
そのため、以下のように定義すると、<EnhancedTextContent text="xyz" />
としてもxyz
ではなくfoo
と表示される。
<WrappedComponent {...this.props} text="foo" color={this.state.color} onClick={this.changeColor} />
this.props.children
ちなみにこのケースでは、HOCを使わずthis.props.children
を使った書き方でも実装できる。
function TextContent({text}) { return ( <div> {text} </div> ); } function ListContent({contents}) { return ( <ul> {contents.map(i => <li key={i}>{i}</li>)} </ul> ); } class Wrapper extends React.Component { constructor(props) { super(props); this.state = {color: this.props.color1}; this.changeColor = this.changeColor.bind(this); } changeColor() { const {color1, color2} = this.props; this.setState({color: this.state.color === color1 ? color2 : color1}); } render() { const {color} = this.state; return ( <div style={{color}} onClick={this.changeColor}> {this.props.children} </div> ); } }
<Wrapper color1="black" color2="red"> <TextContent text="xyz" /> </Wrapper> <Wrapper color1="black" color2="orange"> <ListContent contents={['abc', '123', 'def']} /> </Wrapper>
HOCとどちらを採用すべきかは、状況によるのだと思う。
SFCにライフサイクルメソッドを追加する
Reactのコンポーネントを定義する際は、出来るだけSFC(Stateless Functinal Componenens)で定義するのが望ましいとされる。
SFCはstateを持たないため、コンポーネントをステートレスに保てるからだ。
しかしSFCには、componentDidMount
などのライフサイクルメソッドを利用できないという欠点がある。
しかしHOCを使うことで、SFCでもライフサイクルメソッドを利用できるようになる。
function Basic({text}) { return <div>{text}</div>; } function hocFactory(WrappedComponent) { return class extends React.Component { componentDidMount() { console.log('componentDidMount'); } render() { return <WrappedComponent {...this.props} />; } }; } const EnhancedBasic = hocFactory(Basic);
こうすると、EnhancedBasic
をマウントしたときにログが表示されるようになる。
recompose を使う
HOCによるライフサイクルメソッドの追加は、Recompose
というライブラリを使うことによって、より簡単に記述できる。
https://github.com/acdlite/recompose
先程のEnhancedBasic
を定義する場合、次のように書く。
import React from 'react'; import {lifecycle} from 'recompose'; function Basic({text}) { return <div>{text}</div>; } const EnhancedBasic = lifecycle({ componentDidMount() { console.log('componentDidMount'); }, })(Basic);
lifecycle
のなかでsetState
を使うことも出来る。
そしてstateは、元のコンポーネントにpropsとして渡される。
そのため以下のように書くと、componentWillMount
とcomponentDidMount
の時間差を表示するコンポーネントが作られる。
function Basic({time}) { return <div>{time}</div>; } const EnhancedBasic = lifecycle({ componentWillMount() { this.setState({time: new Date().getTime()}); }, componentDidMount() { this.setState({time: new Date().getTime() - this.state.time}); }, })(Basic);
なおRecompose
には、lifecycle
以外にもHOCを便利に使うための様々な機能が用意されている。
https://github.com/acdlite/recompose/blob/master/docs/API.md