HOC(High Order Component, 高階関数)と型定義
問題
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のFC
はFunctionComponent
と同じ、という意味です。
(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