reactjs
React
HOC

RenderPropsの概要とHOCとの違い

たまたまReactの公式ドキュメントを読んでいて、面白そうな書き方があった。
RenderPropsという書き方である。Render Props - React

議論が起こったのが去年の秋くらいなので、日本語の記事を探してもほとんど見つからなかったので記事にした。議論とはこの記事でHOCよりもRenderPropsの方が利点があり、HOCは代替されるべきと言う内容。

個人的には、書き方としてRenderPropsは知っておいて損ではないが、完全に主張に同意できるほど自分で判別がつかない。なので、この記事では、そもそもRenderPropsは何か、何がHOCと違うのかについて整理することに留める。

対象読者

  • RenderPropsを知らない、概要について理解したい
  • けれども英語のドキュメントを読むのは面倒
  • HOCと何が違うのかを知りたい

RenderPropsとは?

<DataProvider render={(data) => (
  <DataReceiver data={data} />
)}/>

親、子、孫の階層関係において、Renderすべき孫のコンポーネントを指定するのではなく、
親のコンポーネントでその孫のコンポーネントを指定することができる。

const Parent = (props) => {
  return (
    <Child
      render={(data) => <GrandChild data={data} />}
      {...props}
    />
  )
};

const Child = (props) => {
  return (
    <div>
      // 何らかの処理
      { props.render(/*何らかのデータ*/data) }
    </div>
  )
};

const GrandChild = (props) => {
  <div>{ props.data }</div>
};

props.childrenと異なるのは、子のコンポーネントのPropsやStateも孫に渡せるという点がある。

const Parent = (props) => {
  return (
    <Child {...props}>
      <GrandChild {...props} />
    <Child/>
  )
};

const Child = (props) => {
  return (
    <div>
      // 何らかの処理
      { props.children } // このコンポーネントのデータを渡すことはできない
    </div>
  )
};

何ができるか?

中間のコンポーネントのデータを親のコンポーネントから孫に渡すように制御できることによって、
中間に位置するコンポーネントは「どの」コンポーネントにPropsやStateを渡すべきか知る責務から解放され、
「どんな」Propsを渡せば良いかだけが関心事になることで、振る舞いを共通化することができる。

// ParentX => SharedChild => GrandChildX
const ParentX= (props) => {
  return (
    <SharedChild
      render={(data) => <GrandChildX data={data} />}
      {...props}
    />
  )
};

// ParentY => Child => GrandChildY
const ParentY= (props) => {
  return (
    <SharedChild
      render={(data) => <GrandChildY data={data} />}
      {...props}
    />
  )
};

const SharedChild = (props) => {
  return (
    <div>
      // 何らかの共通の処理
      { props.render(/*何らかのデータ*/data) }
    </div>
  )
};

HOCとは何が違うか?

HOCについてよくわからない場合は、こちらの記事で概要を説明したので、手前味噌ですがご覧ください。
基本的には、RenderPropsを使える場合には、HOCでそれを表現することもできる。

const withChild = (WrappedComponent) => {
  // 何かしらの共通の処理
  return <WrappedComponent data={data} />
};

// grand_child_x.js
export default withChild(GrandChildX);

// grand_child_y.js
export default withChild(GrandChildY);

しかし、Michael Jackson の記事では、以下の点がHOCと異なると挙げられている。

  1. 孫のコンポーネントに、PropsやStateがどのコンポーネントから渡されたのかがわかりやすい
  2. 名前の衝突が起こりづらい
  3. 型定義が簡単
  4. HOCはJSXの中で動的に使えず、コンポーネントの外で結合させなければならない

確かに、1, 2は、composeなどでHOCが複数利用されている場合はStateがどこで管理されているのがわかりづらかったり、引数の順番で渡されるPropsの値が上書きされる可能性も高まったりする。

compose(
  connect( ... ),
  enhance( ... ),
  withRouter( ... ),
  lifecycle( ... ),
  ...
)(MyComponent);

3については、実際にHOCを書いてみると以下のようなComponentやHOCの型定義が必要になり面倒だと感じることがある。

import react from 'react';
import type ComponentType from 'react';
import type HOC from 'recompose';

// 受け取るPropsの型
type OuterPoprs = {
  ...
};


// 内部のStateの型
type InnerState = {
  ...
};

// コンポーネントに渡す型
type Props = OuterProps & InnerState & { ... };

const someHoc: HOC<Props, OuterProps> = (WrappedComponent: ComopnentType<Props>) => {
  return class _ extends React.Component<OuterProps, InnerState> {
    // 何かしらの共通の処理
    render() {
      return <WrappedCompnoent ... />
    }
  }
};

所感

正直、利点は理解できるのだが、英語の情報ばかりでHOCをやめようと言えるほど自信がない。
一方で、HOCが複雑に絡み合いそうな時(複数のHOCで状態を管理している時など)はRenderPropsを使ったほうが、どこのStateが渡されたのか見通しがつくようになって良さそうだと思った。

参考