Reactを理解する
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を実現しています。特に非同期処理はデータがキャッシュされたりなど、そういった関心を完全にコンポーネントから切り離されるのが良い点です。