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 で実装したほうがいい