😃

HOC(High Order Component, 高階関数)と型定義

2021/04/13に公開

問題

HOCにReact.FCで型定義をしたところ、型が一致しないという問題が発生しました。

HOC(高階関数)の型定義

早速ですがReact.FCの定義を見てみます。

// (1)
type FC<P = {}> = FunctionComponent<P>;

// (2)
interface FunctionComponent<P = {}> {
    (props: PropsWithChildren<P>, context?: any): ReactElement<any, any> | null;
    propTypes?: WeakValidationMap<P>;
    contextTypes?: ValidationMap<any>;
    defaultProps?: Partial<P>;
    displayName?: string;
}

(1)
React.FCのFCFunctionComponentと同じ、という意味です。

(2)
引数と戻り値の型だけ注目して見てみます。すると

引数 (props: PropsWithChildren<P>, context?: any)
戻り値:ReactElement<any, any> | null;

ということがわかります。
なぜ上記2つに注目したかというと、他のプロパティには?がついているように
あってもなくてもよい、つまりオプションだからです。

すると、React.FCはReact.Elementもしくはnullを返さないといけないことがわかりました。
それを踏まえて先程のHOCを確認してみます。

const WithSpinner: React.FC<wrapperProps> = (WrappedComponent) => ({ isLoading, ...otherProps }: injectorProps) => ( ... )

引数と戻り値に注目すると、

引数 :WrappedComponent
戻り値: ({ isLoading, ...otherProps }: injectorProps) => ( ... )

WithSpinnerは戻り値として関数 () => {} を返していますね。

え、「けど関数の中でHTMLをreturnしてますよね?これって値返してませんか?」 って?

1つ大事なことがあります。関数が値を返すのは、その関数を実行したときです。
関数の定義はあくまで関数を定義しているだけで、実行していません。

つまり今回の場合だと、WithSpinner実行時の戻り値に関数を定義しているだけで、それを実行してはいないので、
結果としては戻り値に関数が返されることになります(その関数の実行結果ではないです)。
そのためHTML(正確にはJSX.Element | null)が返ってくることを期待しているReact.FCは使うことができません。

しかし、戻り値として返ってきた関数は実行したらHTML(JSX.Element)が返ってくるはずです。

// 戻り値の関数

({ isLoading, ...otherProps }) => (
  isLoading ?
    <SpinnerOverlay>
      <SpinnerContainer />
    </SpinnerOverlay>
    : <WrappedComponent {...otherProps} />
)

なのでこの関数の場合、実行元にReact.FCで型定義することができます。

// 1. WithSpinnerそのものにはReact.FCを使用していない(WithSpinerを実行したときの戻り値が関数のため)
// 2. WithSpinnerに代入した関数にはReact.FCを使用している(代入した関数を実行したときの戻り値がHTMLのため)

const WithSpinner = (WrappedComponent: wrapperProps): React.FC<injectorProps> => ({ isLoading, ...otherProps }) => (
  isLoading ?
    <SpinnerOverlay>
      <SpinnerContainer />
    </SpinnerOverlay>
    : <WrappedComponent {...otherProps} />
)

上記HOCは下記と同じです。

const WithSpinner = (WrappedComponent: wrapperProps) => {
  const innerFunction: React.FC<injectorProps> = (props) => {
    const { isLoading, ...otherProps } = props

    return isLoading
      ?
      <SpinnerOverlay>
        <SpinnerContainer />
      </SpinnerOverlay>
      : <WrappedComponent {...otherProps} />
  }

  return innerFunction

innerFunctionはあくまでWithSpinnerという関数の中で定義され、戻り値として返されることに気をつけてください。
それぞれ別の関数として切り出されているわけではありません。

// こうではない
const innerFunction = () => (.A..) // 定義
const WithSpinner = () => { return innerFunction } // 戻り値として返す

// こうなっている
const WithSpinner = () => {
  const innerFunction = () => (...) // 定義

  return innerFunction // 戻り値として返す
}

これがわかると、高階関数は関数の連続実行関数の連続返却であるということがわかります。
外側の関数を実行してから、実行結果(関数)を返却します。

今回だと、
const WithSpinner = (WrappedComponent: wrapperProps): { ... }
の部分を実行することでまずWrapされるComponentを決定し、その後
({ isLoading, ...otherProps }) => (...)
という、戻り値として得られた関数をreturnしています。

そうすると、

外側の関数:WrapするComponentを決める(決めて、HTMLを算出する関数を返す)
内側の関数:HTMLを算出する

というように、責務(Componentの決定、HTMLの算出)が切り離されていることがわかります。
実行するときも、外側の関数を実行してから内側の関数を実行する、という流れになっています。

// 外側の関数を実行(WrapされるComponentの定義して、HTMLを生成する関数を返す)
const executeOuterFunction = WithSpinner(componentForOuterFunction)

// 内側の関数を実行(HTMLを計算、出力)
const executeInnerFunction = executeInnerFunction(propsForInnerFunction)

まとめて実行する場合、下記のようになります

const howToExecuteWholeWithSpinner = WithSpinner(componentForOuterFunction)(propsForInnerFunction)

思ったこと

数学の問題でx + y + z = 1という式があった場合、まずはz = - x - y + 1のようにすると思います。
そもそもなぜこのような形にするのでしょうか?

前者の場合、左辺が3変数、右辺が定数ですよね。
それを後者のようにzを基準として相対的に2変数の式として捉える、
つまりzを固定して考えると2変数の式として見ることができるので、
問題をより簡単に考えることができるから、だと思っています。

次数が多い場合も次数を下げて対処しますよね。平方完成や因数分解もこの考え方に基づいてると思ってます。

今回もそれと同じだと思っていて、WithSpinnerもWrappedComponentとisLoading(+otherProps)で2変数以上ありますが、
まずcomponentを決め打ち(固定)して、あとはisLoadingの値によって出力を変える(1変数で考える)だけで良い状態にする、ということかなと思っています。

そのおかげで関数を別々に定義して関数の中で関数を呼ぶよりも、ずっと使いやすくなっているように思います。

まとめ

  • 高階関数は関数の連続実行 + 関数の連続返却である
  • 関数を戻り値として返却する場合、返却しているだけで実行しているわけではないことに注意

Discussion