recompose簡易解説

記事内でrecomposeとの比較を持ち出すことがあるのですが、このブログに取り扱った記事がないため書いてみることにしました。

以下のAPIを抜粋して取り上げます。

recomposeとは

関数コンポーネントに状態やライフサイクルなど、クラスベースがもっている機能を実装させることができるユーティリティパッケージです。大半の機能はベースとなるコンポーネントをラップして利用します。俗に言う高階コンポーネント(HOC)です。

以降、実例を記載して解説します。

withState

withState(状態名, 更新関数, 初期値)で、関数コンポーネントに状態を持たすことができます。

import React from "react";
import ReactDOM from "react-dom";
import { withState } from "recompose";

const enhanceWithState = withState("name", "setName", "mda");

const BaseUser = ({ name, setName }) => (
  <div>
    <h2>User: {name}</h2>
    <form>
      <input
        type="text"
        defaultValue={name}
        onChange={e => setName(e.target.value)}
      />
    </form>
  </div>
);

const User = enhanceWithState(BaseUser);

function App() {
  return (
    <div>
      <User />
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

BaseUserのpropsに状態名nameと更新用の関数のsetNameが入ります。setNameの返り値が新しい状態値となります。

withStateHandlers

withStateHandlers(状態オブジェクト, 更新用ハンドラ関数)で、複数のハンドラを結びつけることができます。

import React from "react";
import ReactDOM from "react-dom";
import { withStateHandlers } from "recompose";

const enhanceWithHandlers = withStateHandlers(
  { count: 0 },
  {
    increment: ({ count }) => value => ({
      count: count + value
    }),
    decrement: ({ count }) => value => ({
      count: count - value
    })
  }
);

const BaseCounter = ({ count, increment, decrement }) => (
  <div>
    <h2>{count}</h2>
    <button onClick={() => increment(1)}>+</button>
    <button onClick={() => decrement(1)}>-</button>
  </div>
);

const Counter = enhanceWithHandlers(BaseCounter);

function App() {
  return (
    <div>
      <Counter />
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

これも単にpropsに状態とハンドラ関数が追加されているので、それをBaseCounterから呼び出しているだけです。

lifecycle

ライフサイクル機能を関数コンポーネントに持たすことができます。componentDidMountなどのライフサイクルメソッド名と同じ名前の関数をlifeCycle()内で定義します。

import React from "react";
import ReactDOM from "react-dom";
import { lifecycle } from "recompose";

const withLife = lifecycle({
  componentDidMount() {
    console.log("マウント");
  }
});

const BaseHello = () => <p>Hello</p>;
const Hello = withLife(BaseHello);

function App() {
  return (
    <div>
      <Hello />
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

これでHelloコンポーネントがマウントされたときに、コンソールログが出力されます。

compose

composeは、複数のHOCを組み合わせができます。

例えば、以下のように3つのHOCを利用するコンポーネントがあるとします。withX, withY, withZはNULLチェックや何かしらの機能を追加する高階コンポーネントとします。

const A = withX(Item);
const B = withY(A);
const C = withZ(B);
const Item = (props) => return(/* 何かしらを返す */);

const App = () => <C />;

上記のように複数のHOCがある場合、1-3行目コードは他人が見る場合わかりづらいです。以下のようにも書けますが、これも見やすいとは言えません。

const Hoc = withZ(withY(withX(Item)));

composeを使うと以下のように書けます。

const Hoc = compose(withZ, withY, withX);

// 利用時
const HocItem = HOC(Item);
const App = () => <HocItem />;

recomposeを使っている人ならば、記述方法が統一されるので理解しやすいです。

また、composeのAPIを組み合わせて使うことが多いようです。下記では、withHandlers(ハンドラ追加のAPI)のハンドラ定義内でwithStateの更新関数を利用しています。

import React from "react";
import ReactDOM from "react-dom";
import { withState, withHandlers, compose } from "recompose";

const BaseButton = ({ count, increment, decrement }) => (
  <div>
    <h1>{count}</h1>
    <button onClick={() => increment()}>+</button>
    <button onClick={() => decrement()}>-</button>
  </div>
);

const enhance = compose(
  withState("count", "updateCount", 0),
  withHandlers({
    increment: ({ updateCount }) => () =>
      updateCount(prevState => prevState + 1),
    decrement: ({ updateCount }) => () =>
      updateCount(prevState => prevState - 1)
  })
);

const HocButton = enhance(BaseButton);

const App = () => <HocButton />;

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

composeは引数の順番通りに実行されるため、withHandlers内でwithStateの状態や状態更新関数を呼び出すことができます。これらの値はpropsを介して取得できます。

また、lifecycleのようなAPIの中では、this.propsを介してアクセスすることができます。

const enhance = compose(
	withStateHandlers(
	  { time: 0 },
	  { setTime: ({time}) => value => ({ time: time + value })}
	),
	lifecycle({
      componentDidMount() {
          setInterval(() => this.props.setTime(5), 5000);
      }
	});
);

mapProps

mapPropsは現在のコンポーネントが持つpropsを新しいpropsに丸ごと置き換えます。以下はlistというpropsをフィルタリングして、新しいlistとしてセットしています。

const enhance = compose(
  withState("entry", "setEntry", "くさ"),
  mapProps(({ list, entry, setEntry }) => {
    return {
      setEntry,
      entry,
      list: list.filter(monster => monster.type === entry)
    };
  })
);

コードが少し長いため抜粋しましたが、全文はサンプルデモで確認できます。

ラジオボタンの選択のたびsetEntryを呼び出し、entry値を変更しています。その値は上記のlistでフィルタリングされ新しいlistとして置き換わります。表示する内容(ここではポケモン一覧)自体が状態にあるわけではなく、状態entryはmapPropsでlistをフィルタリングする条件に使われているだけです。

おわりに

今後はrecomposeの代わりにReact Hooksが使われることになっていくとは思いますが、強力なライブラリであることに変わりはないため、目にする機会もあると思われます。