react-router の mjackson が HOC を批判していたので、自分の考えを書いておきます。

Use a Render Prop! - componentDidBlog https://cdb.reacttraining.com/use-a-render-prop-50de598f11ce

mjackson の主張の要約

  • ES2015 Classes は mixin 的な振る舞いをサポートしていない
  • HOC は 新しい mixin だ

主張1: HOC は自身の振る舞いを自己規定するものである

const connector = ReactRedux.connect(...)
compose(
  connector,
  pure,
  lifecycle({
    componentDidMount() {
      console.log('mounted')
    }
  })
)(Counter)

これは

  • store に connect され
  • pure であり
  • lifecylcle をもつ

というのは自己完結していて、外から与えられる属性ではなく、内側へ向けた定義です。ReactRedux の connect は文脈無視して props をシングルトンな store から捻り出すので、親から与えられる属性ではなく、自己定義的です。

自分は すべての Component が Stateless Functional Component として定義されるべきだと思ってるので、振る舞いを明示的に切り離す HOC は有意義であると思っています。

主張2: render props はライブラリが外から子の振る舞い(props)を規定するものである

とはいえ mjackson の主張に則れば Redux の connector は次のように書き直すこともできそう。

<Connector
  mapStateToProps={state => state.counter}
  render={props => <Counter value={props.value}/>}
/>

こういうパターンも見ますね。

<Connector
  mapStateToProps={state => state.counter}
>
  {props => <Counter value={props.value}/>}
</Connector>

これはAPIスタイル問題で、ライブラリ作者がどれを選ぶかという問題だと思うんですが、ユーザー側が選べる問題ではなく、
ライブラリ作者はとりあえず render props を採用する、というのはアリだと思います。

自己定義的なHOCはこれらのパターンからハズレて、別の役割を持っていると思います。

自分の思う HOC アンチパターン

とはいえ HOC が無駄に物事を複雑にするケースがあるという主張は理解できます。
例えばこういう形式のコードです。

compose(
  (hocA: HOC<A, B>),
  (hocB: HOC<B, C>),
  (hocC: HOC<C, D>)
): HOC<A, D>

これらが組み合わさって複雑になるときに、それぞれのHOCの順序に依存する、手を付けられない魔物が生まれるのは何度か経験しました。たとえば form 系ライブラリのバインディングと mapStateToProps が混ざるときです。

なので、 「state を扱う HOC は一個までとする」 みたいな縛りが必要だとは思っています。

まとめ

  • 暗黙に他のhocの順序または親のpropsに依存する HOC はいずれにせよアンチパターンである。
  • HOC は自己規定であり、render prop はライブラリの削除
  • ライブラリ作者は render prop で実装したほうがいい