ReactのFunctionComponentでライフサイクル

こんにちは。Boostnoteのメンテナのsosukesuzukiです。BoostnoteはReactで書かれていています。そして実はBoostnoteは作られてから結構時間が経っていて至る所に肥大化したコンポーネントが存在します。Boostnoteでは肥大化したコンポーネントはコントリビュータの皆様のおかげもあって以前と比べたらマシになってはいますが、放置され気味になっています。それについていつかなんとかしたいと考えていて、なんとかする方法についての記事です。

極力StatelessFunctionalComponent

肥大化したコンポーネントは読みにくく、管理がしづらいので、適切な大きさにコンポーネントを分割していくことがが必要です。分割していく上で重要なのは省けるStateは全て省きコンポーネントは極力StatelessFunctionalComponentにすることです。extends React.ComponentではなくStatelessFunctionalComponentとして実装することで状態を保持していないことが自明になります。

// StatelessFunctionalComponentの例

const component = ({hoge, huga}) => (
  <h1>{hoge}</h1>
  <h2>{huga}</h2>
)

ある程度コンポーネントが状態を保有するのは仕方がないことだとしても、状態の管理に気を遣わなければならない箇所はできるだけ増やしたくはありません。そんな所まで頭が回りません。

ライフサイクルはHigherOrderComponent(HOC)

そんな理由があって、StatelessFunctionalComponentを使う方が人間にとって結果的に易しくなることが多いわけですが、アプリケーションを作る上での欠点として、StatelessFunctionalComponent単体ではlifecycleを扱うことができません。それを実現するためにはHighrtOrderComponentを使う必要があります。 HigherOrderComponentはReactコンポーネントを引数として受取りReactコンポーネントを引数として返す関数により実装されます。型シグネチャをHaskellライクに表すと次のようになるでしょう。 hocFactotryがHOC(HigherOrderComponent)のファクトリーとなる関数の名前、Wが引数に渡すReactコンポーネント、Eが返り値のReactコンポーネントです。

HigherOrderComponent:: W: React.Component => E: React.Component

HOCはコンポーネントの抽象性を高め、さらなる再利用を可能にします。 HOCを使うとStatelessFunctionalComponentのライフサイクルをフックすることができます(それ以外にも様々なことに使えますが、この記事ではHOCについての詳しい解説はしません。FaceBookの公式ドキュメント、franleplant氏のReact Higher Order Component in depthなどが非常にわかりやすくHOCについて解説しています。)。

// HOCでStatelessFunctionalComponentのライフサイクルをフックする例
const InputComponent = ({hoge, boo}) => (
  <h1>{hoge}</h1>
  <h2>{boo}</h2>
)

function logProps(WrappedComponent) {
  return class extends React.Component {
    componentWillReceiveProps(nextProps) {
      console.log('Current props: ', this.props);
      console.log('Next props: ', nextProps);
    }
    render() {
      return <WrappedComponent {...this.props} />;
    }
  }
}

const EnhancedComponent = logProps(InputComponent);

この方法をとるにしてもHOCで返すコンポーネントはextends React.Componentを使って記述する必要があります。この形をとるコンポーネントが増えていくと、class宣言とrenderはボイラープレートになっていきます。

recomposeでHOCを楽に

recomposeというライブラリがあります。このライブラリはHOCのためのユーティリティ関数群で、react版lodashと例えられているのを見たこともあります。このライブラリには様々な関数があり、それらはrecomponseのGithubでドキュメントを見ることができます。 上述の通りHOCには様々な使い方があって、それらのための関数が大半でライフサイクルに関係する関数はごく一部です。なので、比較的簡単に導入することはできるんじゃないでしょうか。このあたりは本当にlodashみたいですね。適宜必要な関数だけ使えばよくて、ライブラリ自体が複雑な構造になっているということはありません。 HOCでライフサイクルを楽に実装する上で必要な関数はlifecycle関数です。 lifecycle関数は次のような型シグネチャを持ちます。

lifecycle :: spec: Object => H: HigherOrderComponent 

そして、このようにして使われます。

const PostsList = ({ posts }) => (
  <ul>{posts.map(p => <li>{p.title}</li>)}</ul>
)

const PostsListWithData = lifecycle({
  componentDidMount() {
    fetchPosts().then(posts => {
      this.setState({ posts });
    })
  }
})(PostsList);

lifecycleの引数の中でのStateは、HOCの引数に渡すコンポーネントにpropsとして渡されます。 また、lifecycleの型シグネチャは次のように書き換えることもできます。

lifecycle :: spec: Object => W: React.Component => E:Component

HOCの型がHOC:: W: React.Component => E: React.Componentなので当然なのですが、Haskellやらそのあたりの人にはこのように書いた方がわかりやすいかもしれません。

よさそう

reomposeでHOCを使えばStatelessFunctionalComponentで簡単にライフサイクルまで扱うことができるようになります。状態について考えることがなくなるのも記述が簡潔になるのも人間にとってはよいことだと思うので、使える機会がありそうだったらどんどん使っていったらいいと思います。時間があればBoostnoteでもこのようにコンポーネントを適度に分割していきたいと思っています。