React入門 ~Recompose編~

React入門記事、第5弾。
今回は主に関数コンポーネントに機能を付与することに使われるRecomposeについてです。

※この記事はQiitaからの転載です。

Recomposeとは? #

公式:GitHub - recompose

前提知識として、Reactには高階コンポーネント (Higher-Order Component)という概念があります(通称HOC)
具体的には、あるコンポーネントを受け取って、それに機能を付与した新規のコンポーネントを返すような関数のことを指します。
これまでの記事でもHOCと書いていたものは、これのことでした。

HOCを利用することで、HOC側にロジック、コンポーネント側はビューといったように責務を分離させたり、ロジック部分を複数のコンポーネントで再利用したりといったことができます。
また、stateやライフサイクルを持てない関数コンポーネントに、これらの機能を付与することもできます。

RecomposeはこのHOCを扱うユーティリティ的なライブラリです。
以前は多くの方々に利用されていましたが、React 16.8で追加されたReact Hooksにより、React本体だけでも同様のことができるようになりました。

そのためライブラリの更新はすでに止まっており、今後は使われなくなっていくのではないかと思いますが、業務で使用する機会があったので今回記事に書くことにしました。

インストール #

$ yarn add recompose

今回使用するバージョンは0.30.0です。

使い方 #

以下、記載しているコードは公式サンプルのコードを元にしています。

基本的な使い方 #

HOCを使う場合、主に以下のような書き方をします。

const enhance = HOC(Component);

ここでの定数名、HOC名、コンポーネントはあくまで仮のものですが、その中身としては、以下のようなものになります。

  • enhance:機能が追加され、新たに作成されたコンポーネント
  • HOC:引数のコンポーネントに何らかの処置を施す関数
  • Componet:元となるコンポーネント

元となるコンポーネントをHOCでラップするイメージですね。

これを踏まえたうえで、Recomposeが提供するHOCを使用した例がこちら。

import React from 'react';
import { withState } from 'recompose';

const enhance = withState('counter', 'setCounter', 0);

const Component = enhance(({ counter, setCounter }) => {
  return (
    <div>
      <p>カウンター: {counter}</p>
      <button onClick={() => setCounter(n => n + 1)}>Increment</button>
      <button onClick={() => setCounter(n => n - 1)}>Decrement</button>
    </div>
  );
});

export default Component;

withStateを使用したカウンターのGIF

上記ではstateを扱えるようにするwithStateで、ビュー側であるコンポーネントをラップするようになっています。withStateで定義したstateとstateを更新する関数はpropsに渡されるので、そこから使用できます。

複数のHOCを併用するやり方 #

Recomposeではいろんな種類のHOCが提供されているので、それらを併用して使用したい場合もあると思います。

その場合、普通にやろうとすると以下のようになります。

const enhance = HOC1(HOC2(Component));

実際のコードにするとこんな感じです。
withStatewithHandlersの組み合わせ。

import React from 'react';
import { withState, withHandlers } from 'recompose';

const stateEnhance = withState('counter', 'setCounter', 0);

const handleEnhance = withHandlers({
  incrementCounter: props => () => {
    props.setCounter(v => v + 1)
  },
  decrementCounter: props => () => {
    props.setCounter(v => v - 1)
  },
});

const Component = stateEnhance(handleEnhance((
  { counter, incrementCounter, decrementCounter }) => {
  return (
    <div>
      <p>カウンター: {counter}</p>
      <button onClick={incrementCounter}>Increment</button>
      <button onClick={decrementCounter}>Decrement</button>
    </div>
  );
}));

export default Component;

この例では2つのHOCなのでまだいい方ですが、これがさらに数が増えるとラップする数が増えて可読性が落ちてえらいことに…。
また、先に書いたHOCから実行されるので、上記のようにwithHandlersのなかでwithStateで定義したものを使用している場合は、withStateの方を先に書く必要があります。

この可読性の問題を解消するためにはcomposeという関数を使うとよいです。
composeを使うと、以下のように書くことができます。

const enhance = compose(HOC1, HOC2)(Component);

実際のコードだとこんな感じです。
複数のHOCをまとめて書けるので、すっきりしますね。

import React from 'react';
import { compose, withState, withHandlers } from 'recompose';

const enhance = compose(
  withState('counter', 'setCounter', 0),
  withHandlers({
    incrementCounter: props => () => {
      props.setCounter(v => v + 1)
    },
    decrementCounter: props => () => {
      props.setCounter(v => v - 1)
    }
  })
)

const ComposeComponent = enhance(
  ({
    counter,
    incrementCounter,
    decrementCounter
  }) => {
    return (
      <div>
        <p>カウンター:{counter}</p>
        <button onClick={incrementCounter}>Increment</button>
        <button onClick={decrementCounter}>Decrement</button>
      </div>
    )
})

export default ComposeComponent;

Reduxとの併用 #

Reduxと併用したい場合は、react-reduxのconnectを使うとよいです。
このconnectもHOCが使われているそうで、同様にcomposeで他のHOCとまとめて書くことができます。

import React from 'react';
import { connect } from 'react-redux';
import { compose } from 'recompose';
import { bindActionCreators } from 'redux';
import { incrementOn, decrementOn } from '../../Actions/Counter';

const enhance = compose(
  connect(
    state => ({
      counter: state.counter
    }),
    dispatch => ({
      actions: bindActionCreators({ incrementOn, decrementOn }, dispatch)
    })
  )
)

const ComposeComponent = enhance(
  ({ counter, actions }) => {
    return (
      <div>
        <p>カウンター:{counter}</p>
        <button onClick={actions.incrementOn}>Increment</button>
        <button onClick={actions.decrementOn}>Decrement</button>
      </div>
    )
})

export default ComposeComponent;

HOCの種類 #

数が多いので一部のみ紹介。

無駄な再レンダリングを抑制する:pure #

propsが変更されない限り、コンポーネントが更新されないようにします。
変更されたことを検知するロジックとしてはshallowEqualが使われているようです。

import React from 'react';
import { pure } from 'recompose';

const enhance = pure;

const Component = enhance(
  () => {
    return (
      <div>
        <p>pure test</p>
      </div>
    );
});

export default Component;

propsを置き換える:mapProps #

現在のpropsを関数が返すものに置き換えます。

mapProps実行後は、num1num2はなくなり、sumというpropsのみに置き換えられます。

使用例(propsに num1={10}、num={20} 指定)

import React from "react";
import { mapProps } from 'recompose';

const enhance = mapProps(props => {
  return {
    sum: props.num1 + props.num2
  }
})
const Component = enhance(({ num1, num2, sum }) => {
  return (
    <div>
      <p>{num1 ? num1 : 'propsなし'}</p>
      <p>{num2 ? num2 : 'propsなし'}</p>
      <p>{sum}</p>
    </div>
  );
});

export default Component;

mapPropsを使ったサンプル画像

propsを追加する:withProps #

現在のpropsに関数が返すものを追加します。

使用例(propsに num1={10}、num2={20} 指定)

import React from "react";
import { withProps } from 'recompose';

const enhance = withProps(props => {
  return {
    sum: props.num1 + props.num2
  }
})
const Component = enhance(({ num1, num2, sum }) => {
  return (
    <div>
      <p>{num1 ? num1 : 'propsなし'}</p>
      <p>{num2 ? num2 : 'propsなし'}</p>
      <p>{sum}</p>
    </div>
  );
});

export default Component;

withPropsを使ったサンプル画像

指定したpropsが変更された時のみ、propsを追加する:withPropsOnChange #

基本的にはwithPropsと同じであるものの、こちらは指定したpropsが変更された場合のみpropsの追加が行われます。

使用例

import React from "react";
import { withPropsOnChange } from 'recompose';

const enhance = withPropsOnChange(['num'], props => {
  return {
    sum: props.num * 2
  }
})
const Component = enhance(({ num, sum }) => {
  return (
    <div>
      <p>{num ? num : 'propsなし'}</p>
      <p>{sum ? sum : 'propsなし'}</p>
    </div>
  );
});

export default Component;

propsにnumを指定しなかった場合
withPropsOnChangeを使って、propsを指定しなかった場合のサンプル画像

propsにnumを指定した場合
withPropsOnChangeを使って、propsを指定した場合のサンプル画像

propsのデフォルト値を指定する:defaultProps #

React本体で使用できるdefaultPropsプロパティとほぼ同じことができるものの、厳密には違う模様。
なお、コンポーネント呼び出し時に対象のpropsが指定されていた時は、そちらが優先して使われます。

使用例(propsに指定なし)

import React from "react";
import { defaultProps } from 'recompose';

const enhance = defaultProps({
  text: 'default'
})

const Component = enhance(({ text }) => {
  return (
    <div>
      <p>{text}</p>
    </div>
  );
});

export default Component;

defaultPropsを使ったサンプル画像

propsの名前を変更する:renameProp #

第1引数の名称のpropsを第2引数の名称にリネーム。
このHOC1つにつき、1つしか書けません。

使用例(propsに text=”テスト” を指定)

import React from "react";
import { renameProp } from 'recompose';

const enhance = renameProp('text', 'renameText');

const Component = enhance(({ text, renameText }) => {
  return (
    <div>
      <p>{text ? text : 'propsなし'}</p>
      <p>{renameText ? renameText : 'propsなし'}</p>
    </div>
  );
});

export default Component;

renamePropを使ったサンプル画像

一度に複数のpropsの名前を変更する:renameProps #

renamePropsの複数版。

使用例(propsに text=”テスト” num={10} を指定)

import React from "react";
import { renameProps } from 'recompose';

const enhance = renameProps({
  'text': 'renameText',
  'num': 'renameNum'
});

const Component = enhance(({ text, renameText, num, renameNum }) => {
  return (
    <div>
      <p>{text ? text : 'propsなし'}</p>
      <p>{renameText ? renameText : 'propsなし'}</p>
      <p>{num ? num : 'propsなし'}</p>
      <p>{renameNum ? renameNum : 'propsなし'}</p>
    </div>
  );
});

export default Component;

renamePropsを使ったサンプル画像

平坦化したpropsを追加する:flattenProp #

あくまで平坦化したpropsを追加なので、平坦化の元になったpropsもそのまま残ります。

使用例(propsに obj={{‘a’: ‘A’, ‘b’: ‘B’, ‘c’: ‘C’}} を指定)

import React from "react";
import { flattenProp } from 'recompose';

const enhance = flattenProp('obj');

const Component = enhance(({ obj, a, b, c }) => {
  return (
    <div>
      <p>{obj.a}{obj.b}{obj.c}</p>
      <p>{a}</p>
      <p>{b}</p>
      <p>{c}</p>
    </div>
  );
});

export default Component;

flattenPropを使ったサンプル画像

stateを追加する:withState #

第1引数にstate名、第2引数にstateを更新する関数、第3引数にデフォルト値を指定します。
stateを更新する関数を使用する際の引数は、ただ設定値だけを渡すほかに、現在の値を引数とした処理を記述することも可能です。
デフォルト値の指定に関しても、単純な値のほかにコールバック関数も指定できます。

使用例(基本的な使い方の例と同じです)

import React from 'react';
import { withState } from 'recompose';

const enhance = withState('counter', 'setCounter', 0);

const Component = enhance(({ counter, setCounter }) => {
  return (
    <div>
      <p>カウンター: {counter}</p>
      <button onClick={() => setCounter(n => n + 1)}>Increment</button>
      <button onClick={() => setCounter(n => n - 1)}>Decrement</button>
    </div>
  );
});

export default Component;

withStateを使用したカウンターのGIF

関数ハンドラーを追加する:withHandlers #

定義した関数ハンドラーにはpropsが渡されるので、その値を使った処理を記述することができます。

使用例(複数のHOCを併用するやり方の例と同じです)

import React from 'react';
import { compose, withState, withHandlers } from 'recompose';

const enhance = compose(
  withState('counter', 'setCounter', 0),
  withHandlers({
    incrementCounter: props => () => {
      props.setCounter(v => v + 1)
    },
    decrementCounter: props => () => {
      props.setCounter(v => v - 1)
    }
  })
)

const ComposeComponent = enhance(
  ({
    counter,
    incrementCounter,
    decrementCounter
  }) => {
    return (
      <div>
        <p>カウンター:{counter}</p>
        <button onClick={incrementCounter}>Increment</button>
        <button onClick={decrementCounter}>Decrement</button>
      </div>
    )
})

export default ComposeComponent;

※プレビューはwithStateの例と同じなので省略

stateと関数ハンドラーを追加する:withStateHandlers #

stateと、そのstateに関する関数ハンドラーをまとめて定義したい時は、こちらを使用。

使用例(propsに指定なし)

import React from 'react';
import { withStateHandlers } from 'recompose';

const enhance = withStateHandlers(
  ({ initialCounter = 0 }) => ({
    counter: initialCounter,
  }),
  {
    incrementOn: props => () => ({
      counter: props.counter + 1,
    }),
    decrementOn: props => () => ({
      counter: props.counter - 1,
    }),
    resetCounter: (_, { initialCounter = 0 }) => () => ({
      counter: initialCounter,
    }),
  }
)

const ComposeComponent = enhance(
  ({ counter, incrementOn, decrementOn, resetCounter }) => {
    return (
      <div>
        <p>カウンター:{counter}</p>
        <button onClick={incrementOn}>Increment</button>
        <button onClick={decrementOn}>Decrement</button>
        <button onClick={resetCounter}>Reset</button>
      </div>
    )
})

export default ComposeComponent;

withStateHandlersを使用したカウンターのGIF

ローカルReducerを追加する:withReducer #

Actionを発行して、そのタイプに応じた状態の更新を行うReduxライクな処理を書くことができます。
Reduxを使うまでではないが、より複雑な状態管理を行いたいという時に向いています。

import React from 'react';
import { compose, withReducer, withHandlers } from 'recompose';

const counterReducer = (count, action) => {
  switch (action.type) {
    case 'INCREMENT':
      return count + 1
    case 'DECREMENT':
      return count - 1
    default:
      return count
  }
}

const enhance = compose(
  withReducer('counter', 'dispatch', counterReducer, 0),
  withHandlers({
    incrementOn: props => () => props.dispatch({type: 'INCREMENT'}),
    decrementOn: props => () => props.dispatch({type: 'DECREMENT'}),
  })
);

const ComposeComponent = enhance(
  ({ counter, incrementOn, decrementOn }) => {
    return (
      <div>
        <p>カウンター:{counter}</p>
        <button onClick={incrementOn}>Increment</button>
        <button onClick={decrementOn}>Decrement</button>
      </div>
    );
});

export default ComposeComponent;

※プレビューはwithStateの例と同じなので省略

ライフサイクルを追加する:lifecycle #

componentDidMountをはじめとした、ライフサイクルを追加できます。

使用例

import React from 'react';
import { compose, withState, lifecycle } from 'recompose';

const enhance = compose(
  withState('text', 'setText', ''),
  lifecycle({
    componentDidMount() {
      this.props.setText('initial');
    }
  })
)

const ComposeComponent = enhance(({ text }) => {
    return (
      <div>
        <p>{text}</p>
      </div>
    )
})

export default ComposeComponent;

lifecycleを使用したサンプル画像


Recomposeは業務で使用した経験があったので、記事に起こすのそんなに難しくないと思いきや、思いのほか機能が多かったです(苦笑)
自分が使った機能はほんの一部にすぎなかったようです。

またもや長くなって力尽きたので、一部機能のみ紹介にしました。
気が向いたら他の機能も書くかも?

今後使われなくなっていくと思われるライブラリではありますが、保守案件とかで触れる機会があるかもしれないので、さらっとした知識は一応持っておきたいですね。

参考リンクまとめ #

シリーズ記事 #