2022-03-08 15:00:00

【React】高階関数はちゃんと高階関数自身もmemo化した方が良さそう。

下のように、関数を返却する関数のカスタムHookを作ったとします。

const useFactory = (dep1: DepType1) => {
  const factory = useCallback((arg1: ArgType1) => (arg2: ArgType2) => {
    switch(arg1) {
        // - arg1の値に応じて処理を切り替える //
        // - dep1を使った何らかの計算を行う //
    }
  }, [dep1])
  return factory
}

このカスタムHookには実は問題があって、それはuseCallbackにより、dep1の値が変わらない限りは常に同じfactory関数を取得することができます。しかし、factory関数がarg1を使って生成する関数は、メモ化されていないので実行されるたびに毎回新しい関数を生成します。

これはちょっとだけ問題があります。例えば下記のような時です。

// プロパティでonEventを受け取る。React.memoしているのでonEventが変わらない限りは再計算されない
const MemoSaretaComponent: React.FC<Props> = React.memo(({ onEvent }) => {
  ...
})

const PageComponents = () => {
  const dep1 = useDep1()
  const arg1 = useArg1()
  // factoryはuseCallbackでメモ化されている
  const factory = useFactory(dep1)
  // factory関数が生成する関数はメモ化されないので、func1は毎回違う値を返却する
  const func1 = factory(arg1)
  // func1が毎回違う値を返却してしまうので、コンポーネントは毎回再計算されてしまう
  return <MemoSaretaComponent onEvent={func1} />
}

上記で問題になるのは、const func1 = factory(arg1)の箇所で、このfunc1に代入される関数はPageComponentの処理が実行されるたびに新しく生成されてしまいます。そのせいで、たとえdep1、arg1の値に変化がなかったとしても、実行されるたびにMemoSaretaComponentは新しく作り替えられてしまい、メモ化の意義がなくなります。

これを解決するにはどうすればいいのでしょうか?Hooksは仕様上、下記のようにHookの中でHookを呼び出すことはできないです 🤔

  // こんな風にができれば解決なんだけどできない。。。
  const func1 = useMemo(() => {
      const factory = useFactory(dep1)
      return factory(arg1)
  }, [dep1, arg1])

一応、下のコードのように、依存関係にfunc1useCallbackでメモし、dep1arg1depsに指定してやれば解決できますが、このようなカスタムHook側にIFだけではわからない、前提条件を課すような実装は極力避けるべきです。

  // factoryはuseCallbackでメモ化されている
  const factory = useFactory(dep1)
  // factory関数が生成する関数はメモ化されないので、func1は毎回違う値を返却する
  const func1Orig = factory(arg1)
  const func1 = useCallback(func1Orig, [dep1, arg1]) // これで、一応dep1とarg1が変わらない限り、func1は同じfunc1Origを使うことができる

解決方法:高階関数の戻り値メモする

この問題に対する解決方法の1つとして、高階関数の戻り値をメモ化する方法が使えます。例えば、memoize-oneを使うと良さそうです。 memoize-oneは関数の戻り値をメモ化してくれるライブラリです。このライブラリを使えば、同じ値を引数に渡している限りは関数の再実行をせずに直前の計算結果を再利用してくれる関数です。詳しくはリンク先のサンプルPGを見てもらうとイメージがわかりやすいと思います。 これを使ってfactory関数が返却する関数をメモしてやれば、引数arg1の値が変わらない限り、常に同じ関数オブジェクトを取得することができます。

import memoizeOne from 'memoize-one'

const useFactory = (dep1: DepType1) => {
  // memoizeOneを使うと、useCallbackに指定できないので、代わりにuseMemoを使う。
  // factoryは、dep1の値が変わらない限り、常に同じ関数が得られる。
  const factory = useMemo(
    // memorizeOneでメモ化する。これにより、
    () => memoizeOne((arg1: ArgType1) => (arg2: ArgType2) => {
      switch(arg1) {
        // - arg1の値に応じて処理を切り替える //
        // - dep1を使った何らかの計算を行う //
      }
    }), 
    [dep1]
  )
  return factory
}