Reactでは、HoCsという考え方を用いることでコンポーネントのロジックを抽象化し再利用することができます。これはReact-RouterやMaterial-UI、Apolloなど既に多くのライブラリで使用されています。 コンポーネントからこれらのロジックを引き離すことは、ロジックの再利用性を高めるだけでなく、コンポーネントのテストをとても容易にします。

Component

ReactのcreateElementは純粋関数です。3つの引数を受け取り、オブジェクトを返します。

import { createElement } from 'react'
const element = createElement(
  'h1',
  {className: 'greeting'},
  'Hello, world!'
) 

この関数は実行されると、大体はこのようなオブジェクトを返します。

const element = {
  type: 'h1',
  props: {
    className: 'greeting',
    children: 'Hello, world'
  }
}

JsxではこれをXmlのような構文に置き換えます。

import Ract from 'react'
const element =
  <h1 className="greeting">
    Hello, world!
  </h1>

よってコンポーネントは以下のように宣言できます。

import Ract from 'react'
const Component = props =>
  <h1 className="greeting">
    Hello, world!
  </h1>

コンポーネントはオブジェクトを受け取り、オブジェクトを返します。

HoCs

引数が関数であったり、返り値が関数である関数を高階関数(Higher-order Function)と呼ばれているそうです。 そこで、コンポーネントを受け取りコンポーネントを返す関数はHoCs(Higher-order Components)と呼ばれています。

import Ract from 'react'
const hoc = Component => props =>
  <Component {...props} />
const Component = props =>
  <h1 className="greeting">
    Hello, world!
  </h1>
export default hoc(Component)

この関数はコンポーネントを受け取りそのままコンポーネントを返しているので、特に意味はありません。

Next.js

ここではNext.jsを用いてHoCsの動作を確認していきます。

$ mkdir hocs
$ cd hocs
$ yarn init -y
$ yarn add next react@15 recompose
$ mkdir pages

package.jsonにこのようなscriptを設定して、devを実行します。

"scripts": {
  "build": "next build",
  "dev": "next",
  "start": "next start"
},

次にpagesディレクトリに以下のファイルを生成してください。名前はhello.jsとしておきます。

import React from 'react'

export const withBasic = Component => props => <Component {...props} />

export const Component = props => <p>hello</p>

export default withBasic(Component)

yarn devを実行するとサーバーが起動し、localhost:3000/helloで動作が確認できます。以降のコードは好きな名前をつけて保存してください。

Props

HoCsの基本的な使い方はpropsを追加することです。

import React from 'react'

export const withMsg = Component => props =>
  <Component {...props} msg='hello' />

export const Component = props => <p>{props.msg}</p>

export default withMsg(Component)

このようにwithMsgを実行することでComponetのpropsにはmsgが追加されます。 2つのHoCsを組み合わせるにはこのようにします。

import React from 'react'

export const withMsg = Component => props =>
  <Component {...props} msg='hello' />

export const withHello = Component => props =>
  <Component {...props} hello='msg' />

export const Component = props =>
  <p>{`${props.msg} ${props.hello}`}</p>

export default withMsg(withHello(Component))

withMsgによってprops.msgが追加され、withHelloによってprops.helloが追加されます。

compose

ここからはこのHoCsを組み合わせていくのですが、関数型プログラミングの基本的な知識が必要になります。

import React from 'react'
import compose from 'recompose/compose'

const add = x => y => x + y

const add2 = add(2)
const add4 = add(4)

const result = compose(add4, add2)(2)

export default () => <p>{result}</p>

この関数addはカリー化された関数で部分適用することでadd2とadd4の2つの関数を作ることができます。 この2つの関数を合成する為にcompose関数を使います。このcompose関数はramdaのような関数型のライブラリに大体存在します。数学的には(f・g)(x)が成り立つことと同じです。 部分適用を用いて更に再利用性の高いHoCsを考えます。

import React from 'react'

export const withMsg = msg => Component => props =>
  <Component {...props} msg={msg} />

export const Component = props => <p>{props.msg}</p>

export default withMsg('hello')(Component)

withMsgはmsgを受け取り、それをそのままpropsに追加しComponentに渡すことができます。

withState

recomposeは沢山のHoCsを詰め込んだ素敵なライブラリです。必要なものだけimportするといいです。 withStateを用いることでstateとそれを更新する為の関数を追加することができます。ここでhelloは初期値になっています。

import React from 'react'
import compose from 'recompose/compose'
import withState from 'recompose/withState'

export const Component = props => <p>{props.msg}</p>

export default compose(
  withState('msg', 'setMsg', 'hello')
)(Component)

props.msgはwithStateの内部のthis.stateであり、setMsgではsetStateが呼び出されているので、再レンダリングが可能です。

withHandlers

setMsgを用いてstateを更新する為にonClickを定義します。

これは悪い例です。

import React from 'react'
import compose from 'recompose/compose'
import withState from 'recompose/withState'

export const Component = props =>
  <button onClick={onClick(props)}>
    {props.msg}
  </button>

export const onClick = props => () => { props.setMsg('bad') }

export default compose(
  withState('msg', 'setMsg', 'hello')
)(Component)

まず、レンダリングされるたびにonClickが実行されるのはパフォーマンスがよろしくありません。 次に、Componentがprops以外の変数に依存しているので、純粋関数でなくなってしまっています。

withHandlersを用いてこの関数をpropsにします。

import React from 'react'
import compose from 'recompose/compose'
import withState from 'recompose/withState'
import withHandlers from 'recompose/withHandlers'

export const Component = props =>
  <button onClick={props.onClick}>
    {props.msg}
  </button>

export const onClick = props => () => props.setMsg('good')

export default compose(
  withState('msg', 'setMsg', 'hello'),
  withHandlers({onClick})
)(Component)

withHandlersはprops.setMsgを参照しているので、関数を合成する順番に注意してください。 この2つのHocsは合成して変数に代入しておくことで再利用できます。

import React from 'react'
import compose from 'recompose/compose'
import withState from 'recompose/withState'
import withHandlers from 'recompose/withHandlers'

export const Component = props =>
  <button onClick={props.onClick}>
    {props.msg}
  </button>

export const onClick = props => () => props.setMsg('good')
const composer = compose(
  withState('msg', 'setMsg', 'hello'),
  withHandlers({onClick})
)
export default composer(Component)

さいごに

MobXやReduxはそれに沿ってはいませんが、HoCsにはwithとつけると区別し易くなります。

多くのライブラリはHoCsを用いて、内部でFluxを実現しています。特に非同期処理はデータがキャッシュされたりなど、そういった関心を完全にコンポーネントから切り離されるのが良い点です。