2021/08/27

HOCとRender Propsの違いを理解する。

@ 酒井悠宇

Hooks誕生までのヒストリーがしりたい

昨日書いたこの記事なんですけど、mixin、hoc、renderpropsがそれぞれどういうものなのかを雰囲気感じることはできたのですが、hocとrenderpropsの違いを説明しろと言われたらちょっと無理だなと思ったので、今日はその辺についてまとめていきたいと思います。

HOCとは

hocとは(heiger order component)の略で、高階コンポーネントとも呼ばれます。
簡単に説明すると、「コンポーネントを引数に受け取って、コンポーネントを返す関数」です。

以下はReact公式ドキュメント引用です。

高階コンポーネントとは、あるコンポーネントを受け取って新規のコンポーネントを返すような関数です。


Hooks登場以前のReactでは、stateやライフサイクル等といった機能がクラスコンポーネントにしか用意されていませんでした。
それらの機能を関数コンポーネントに持たせたい時等にHOCが使われていたようです。
イメージとしては、「コンポーネントを引数にとって、その受け取ったコンポーネントをガチャガチャした後のコンポーネントを返り値として返す」みたいな感じです。

「コンポーネントからロジックを切り離して再利用したい」みたいな需要から生まれた設計パターンです。

RenderPropsとは


HOCがどういうものなのかは結構わかってるんですけど、このRender Propsがよくわからないんですよね。
ちょっと丁寧めに進めていきたいと思います。

以下はりあクト引用です。

RenderPropsとは、React Elementsを返す関数をpropsとして受け取って、それを自身のレンダリングに使う特殊なコンポーネントを使った手法です。レンダリングのための関数をpropsとして受け取るからRender Propsと呼びます。


整理すると、HOCは「コンポーネント」を引数に受け取って「コンポーネントを返す『関数』」で、
RenderPropsは、「React Elementsを返す関数」を引数に受け取って、「それを自身のレンダリングに使う『コンポーネント』」っていうことですね。ふむふむ。

「コンポーネント」と「React Elementsを返す関数」


「コンポーネント」と「React Elementsを返す関数」違いはどこにあるのでしょうか。

結論から言うと「コンポーネント」と言うのは「関数、またはクラスコンポーネント」を指し、
「React Elementsを返す関数」と言うのは「関数コンポーネントを」指す。と言うことになります。

その理由を見ていきましょう。

「コンポーネント」についてですが、コンポーネントには2種類あります。
「関数コンポーネント」と「クラスコンポーネント」です。

まずは関数コンポーネントから見ていきましょう。
以下は簡単な関数コンポーネントです。

import React from "react";

export const Test = () => {
  return <button type="submit">ボタン</button>
}


このJSXは「React Elementsを生成する関数」ですね。

<button onClick={onClick}>ボタン</button>


なぜならJSXはコンパイルの結果以下のような関数に変換されるからです。

React.createElement(
  "button",
  { type: "submit" },
  "ボタン"
);


jsxはコンパイルされるとReact.createElementという関数になる。つまり、React Elementsを生成する関数になる。
そして関数コンポーネントというのは、jsxをreturnする関数なので、つまり「関数コンポーネント=React Elementsを返す関数」となりますね。

対してクラスコンポーネントはどうでしょうか。
以下は簡単なクラスコンポーネントになります。

import React, { Component } from "react"

class ClassComponent extends Component {
  render() {
    return <button type="submit">ボタン</button>
  }
}


このJSXは「React Elementsを生成する関数」です。

<button type="submit">ボタン</button>


jsxはコンパイルされるとReact.createElementという関数になる。つまり、React Elementsを生成する関数になる。
そしてクラスコンポーネントというのは、jsxをrenderメソッド内でreturnするクラスです。

なので、「クラスコンポーネント = ReactElementsをrenderメソッドで返すクラス」となります。

よって、「コンポーネント」と言うのは「関数、またはクラスコンポーネント」を指し、
「React Elementsを返す関数」と言うのは「関数コンポーネント」を指す。と言う結論になります。

まとめ


最初に、HOCは「コンポーネント」を引数に受け取って「コンポーネントを返す『関数』」で、RenderPropsは、「React Elementsを返す関数」を引数に受け取って、「それを自身のレンダリングに使う『コンポーネント』」であると説明しました。

そして「コンポーネント」というのは「関数、またはクラスコンポーネント」を指していて、
React Elementを返す関数というのは、「関数コンポーネント」を指していることもわかりました。

以上を前提にもう一度この文章を読んでみましょう。

  • HOCは「コンポーネント」を引数に受け取って「コンポーネントを返す『関数』」
  • RenderPropsは、「React Elementsを返す関数」を引数に受け取って、「それを自身のレンダリングに使う『コンポーネント』」


いい感じに両者の違いがわかりました。

  • HOCは引数としてコンポーネントを受け取るが、Render Propsは引数としてReact Elementを返す関数を受け取る。
  • HOCはコンポーネントを返すが、Render Propsは自身のレンダリングを返す。
  • HOCは関数だが、Render Propsはコンポーネント


大体こんな感じですね。

コードから違いを見てみる


こちらはHOCによるカウンター実装です。

import React, { FC, Component, ReactElement } from "react"
type InjectedProps = {
  count: number,
  reset: () => void,
  increment: () => void,
}
type Props = { max: number }
type State = { count: number }
const withCounter = (WrappedComponent: FC<Props & Partial<InjectedProps>>) =>
  class EnhancedComponent extends Component<Props, State> {
    constructor(props: Props) {
      super(props)
      this.state = { count: 0 }
    }
    reset = (): void => this.setState({ count: 0 })
    increment = (): void => this.setState((state) => ({ count: state.count + 1 }))
    componentDidUpdate = (): void => {
      if (this.state.count > this.props.max) this.reset()
    }

    render = (): ReactElement => (
      <WrappedComponent
        max={this.props.max}
        count={this.state.count}
        reset={this.reset}
        increment={this.increment}
      />
    )
  }
const CounterComponent: FC<Props & Partial<InjectedProps>> = ({
  max,
  count = 0,
  reset = () => undefined,
  increment = () => undefined,
}) => (
  <div>
    <div>
      {count} / {max}
    </div>
    <button onClick={reset} type="button">
      Reset
    </button>
    <button onClick={increment} type="button">
      +1
    </button>
  </div>
)
export const Counter = withCounter(CounterComponent)


使うときはimportしてこんな感じで使います。

<Counter max={10} />


withCounterというHOCが状態とロジックを担当していて、CounterComponentが見た目を担当しているのが一目瞭然ですね。
直感的というか、自分はRender PropsよりもHOCの方がわかりやすいです。

こちらはRender Propsによるカウンター実装です。

import React, { Component, FC, ReactElement } from "react"
type ChildProps = {
  count: number,
  reset: () => void,
  increment: () => void,
}
type Props = {
  max: number,
  children: (props: ChildProps) => ReactElement,
}
type State = { count: number }
class CounterProvider extends Component<Props, State> {
  constructor(props: Props) {
    super(props)
    this.state = { count: 0 }
  }
  reset = (): void => this.setState({ count: 0 })
  increment = (): void => this.setState((state) => ({ count: state.count + 1 }))

  componentDidUpdate = (): void => {
    if (this.state.count > this.props.max) this.reset()
  }
  render = (): ReactElement =>
    this.props.children({
      count: this.state.count,
      reset: this.reset,
      increment: this.increment,
    })
}
const Counter2: FC<{ max: number }> = ({ max }) => (
  <CounterProvider max={max}>
    {({ count, reset, increment }) => (
      <div>
        <div>
          {count} / {max}
        </div>
        <button onClick={reset} type="button">
          Reset
        </button>
        <button onClick={increment} type="button">
          +1
        </button>
      </div>
    )}
  </CounterProvider>
)
export default Counter2


使う時はimportしてこんな感じで使います。

<Counter2 max={10} />


HOCもRender Propsも外側のコンポーネントから状態やロジックを注入したいというのは同じですね。
ただコンポーネントのでき方が違うというかなんというか。

最後に

はい、というわけで今日はHOCのRender Propsの違いについて勉強してみました。
最後にもう一回まとめておくとこんな感じです。

  • HOCはコンポーネントを引数に受け取りコンポーネントを返す関数
  • Render PropsはReact Elementを返す関数を引数に受け取り、自身のレンダリングを返すコンポーネント


自分のイメージですが、

  • HOCは見た目担当とロジック担当の協力プレイでコンポーネントが出来上がるイメージ
  • Render Propsは見た目にロジックが注入されてコンポーネントが出来上がるイメージ

みたいな感じです。

いい感じに両者の違いがわかった気がします。
最後までごらんいただきありがとうございました。

© 2021 powerd by UnReact