LoginSignup
4
6

More than 1 year has passed since last update.

【悲報】React.useCallback() を使いこなせない

Last updated at Posted at 2020-09-19

初めに

みなさん React.useCallback() を使いこなせていますでしょうか。もちろん、僕は使いこなせていません(泣
この記事は、皆さんの助言をいただきながら、みんなで React.useCallback() を、ひいてはその親兄弟親戚一同である React.useMemo() や React.memo() も使いこなせるようになろうではないか、というものです。
といいながらも、だらだらと拙文をお読みいただくのもあれなので、最初に現時点での個人的な結論から書きたいと思います。

結論 (効果音)

メモ化の機能は、値をキャッシュとして保持することにより、処理を高速化したいときに用いる。
ただし、値をキャッシュから読み出すべきか、そうではなく新ためて元のリソースから値を読み出すべきか、という判断がきちんとできないのなら、使用するべきではない。そうであるとすると、各プログラマ単位では、メモ化の機能を使用しなけらばならないケースはそう多くはないのではないか。
具体例としては、「値をファイルから読み込む処理があるが、毎回ファイルから読み込むと処理時間がかかるので、ファイルが更新されていなければ値をキャッシュから読み込む」というようなものが考えられる。よくある処理なので問題はないであろうが、もし「ファイルが更新されているかどうか」というチェックの処理時間が、ファイルから値を読み込む処理時間よりも長いなら、キャッシュ処理はしない方がよいことになる。
この例が示すように、キャッシュ処理は安易に導入すべきものではなく、慎重に設計や実験をしたうえで導入すべきものなのである。
また、React.useCallback() については、関数内関数が毎回作成されることによる弊害を防止するために使えるが、その弊害が「本当に弊害である」ということをきちんと理解したうえで使用するべきであり、「処理が高速化されるかもしれないから」などという安易な理解のみで使用するべきではない。

React.useCallback() による処理の高速化とは

「関数を定義する」、「関数を実行する」

いきなりですが次のソースを見てください。

function sample() {
  // 一連の処理
}

一般的には「関数を定義した」といわれるものです。「関数を定義した」というだけではいかにも抽象的ですので、ここでその意味をきちんと理解してしまいましょう。
次のソースを見てください。

function sample() {
  // 一連の処理
}()

こうすると、「関数を定義し、実行した」とうことになります。
() を付けただけでこうなります。次のように書くのと同じです。

function sample() {
  // 一連の処理
}

sample()

「関数を定義する」と「関数を実行する」とは、それぞれ「一連の処理を定義する」、「一連の処理を実行する」である、という概念がきっちり区別できたら次に進みましょう。できるようになるまではけして進んではいけません。
どうしても理解できない方は、一人で悩まずに、お友達のエンジニアと一緒に議論しましょう。

アロー関数

いわゆるアロー関数を使った方が「関数の定義」の概念をつかみやすくなります。

const sample = () => {
  // 一連の処理
}

「一連の処理を変数に代入する」とか「一連の処理に名前を付ける」とか、理解の仕方はいろいろ考えられるところですが、すでにみたように、けして「一連の処理を実行する」ものではない、ということは理解しておきましょう。「関数を実行」したいのなら次のようにすべきことなります。

const sample = () => {
  // 一連の処理
}()

モジュールレベルでローカルな関数

ここまでの sample() 関数は、モジュールレベルでローカルな関数であることとを想定しています。しかしながら、モジュールという概念は曖昧なものであり、特に JavaScript では、単に名前空間の解決をするためだけにモジュールのロードタイミングを決定しているにすぎず、内部では HTML 内に <Script>タグがいくつかある状態になっているというだけの話です。
そこで、この記事では、モジュールという概念を使用せずに、すぐに実際にコードを実行できる HTML ファイル形式のソースをいちいち掲載します。

sample1.html
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width">
  <title>Test</title>
</head>

<body>
  <div id="App"></div>
</body>

<script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>
<script crossorigin src="https://unpkg.com/htm"></script>

<script>

  // htm is JSX-like syntax in plain JavaScript - no transpiler necessary.
  // https://github.com/developit/htm
  const html = htm.bind(React.createElement)

  const sample = () => {
    // 一連の処理
    let result = "H"
    result += "E" 
    result += "L" 
    result += "L" 
    result += "O"
    return result 
  }

  const elm = html`
  <h1>TEST</h1>
  <p>${sample()}</p>
  <p>${sample()}</p>
  `

  ReactDOM.render(elm, document.getElementById("App"))

</script>

React の記事なので React をロードしているのは当然として、いちいちトランスパイルしなくてもいいように htm というライブラリもロードしています。JSX 記法とほとんど同じことをストリングテンプレートで実現できる優れたライブラリです。
さて、上のソースでは、sample() がモジュールレベル(モジュールという概念は捨てましたが、グローバルレベルというのもなんかピンと来ないのでとりあえずそうしておきます)でローカルな関数であることが分かると思います。
ここで理解しておくべきことは、モジュールレベルでローカルな関数は、モジュールがロードされたときに定義される(モジュールという概念は捨てたのであれですがもう繰り返しません)、ということです。
次のソースがエラーになることは誰でも理解できると思います。

sample2.html
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width">
  <title>Test</title>
</head>

<body>
  <div id="App"></div>
</body>

<script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>
<script crossorigin src="https://unpkg.com/htm"></script>

<script>

  // htm is JSX-like syntax in plain JavaScript - no transpiler necessary.
  // https://github.com/developit/htm
  const html = htm.bind(React.createElement)

  const elm = html`
  <h1>TEST</h1>
  <p>${sample()}</p> <!-- エラー -->
  <p>${sample()}</p>
  `

  const sample = () => {
    // 一連の処理
    let result = "H"
    result += "E" 
    result += "L" 
    result += "L" 
    result += "O"
    return result 
  }

  ReactDOM.render(elm, document.getElementById("App"))

</script>

しかし、次のソースがエラーにならないことを理解できない方がたまにおられます。そういう方は、もう一度この記事の最初からやり直してください。理解できている方は次に進みましょう。
途中で挫折して欲しくありませんので、理解できているふりだけはしないでくださいね。理解できない方はお友達のエンジニアと(以下同文)

sample3.html
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width">
  <title>Test</title>
</head>

<body>
  <div id="App"></div>
</body>

<script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>
<script crossorigin src="https://unpkg.com/htm"></script>

<script>

  // htm is JSX-like syntax in plain JavaScript - no transpiler necessary.
  // https://github.com/developit/htm
  const html = htm.bind(React.createElement)

  const render = () => {
    const elm = html`
    <h1>TEST</h1>
    <p>${sample()}</p> <!-- OK! -->
    <p>${sample()}</p>
    `

    ReactDOM.render(elm, document.getElementById("App"))
  }

  const sample = () => {
    // 一連の処理
    let result = "H"
    result += "E" 
    result += "L" 
    result += "L" 
    result += "O"
    return result 
  }

  render()

</script>

なお、最近のほとんどのブラウザでは、dynamic import() 関数が使えますが、それを使用して同じモジュールを何度もロードしても、モジュールレベルの関数が定義されるのは、最初のロードのときだけです(のはずです。以前実験したことがありそれから変わってないと思いますが・・・助長になるのでここでは確認実験しません、すみません)。

関数内関数

では、sample() 関数を関数内に移動してみましょう。
ただ、その前に、そろそろ React を使い始めましょうか。

sample4.html
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width">
  <title>Test</title>
</head>

<body>
  <div id="App"></div>
</body>

<script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>
<script crossorigin src="https://unpkg.com/htm"></script>

<script>

  // htm is JSX-like syntax in plain JavaScript - no transpiler necessary.
  // https://github.com/developit/htm
  const html = htm.bind(React.createElement)

  const sample = () => {
    // 一連の処理
    let result = "H"
    result += "E" 
    result += "L" 
    result += "L" 
    result += "O"
    return result 
  }

  const Wrapper = props => {

    return html`<p>${sample()}</p>`
  }

  const elm = html`
  <h1>TEST</h1>
  <${Wrapper} />
  <${Wrapper} />
  `

  ReactDOM.render(elm, document.getElementById("App"))

</script>

React 使い始めただけで混乱しないでくださいね。
関数型コンポーネントは文字通りただの関数です。それでは、関数内に関数を移動してみましょう。

sample5.html
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width">
  <title>Test</title>
</head>

<body>
  <div id="App"></div>
</body>

<script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>
<script crossorigin src="https://unpkg.com/htm"></script>

<script>

  // htm is JSX-like syntax in plain JavaScript - no transpiler necessary.
  // https://github.com/developit/htm
  const html = htm.bind(React.createElement)

  const Wrapper = props => {

    const sample = () => {
      // 一連の処理
      let result = "H"
      result += "E" 
      result += "L" 
      result += "L" 
      result += "O"
      return result 
    }

    return html`<p>${sample()}</p>`
  }

  const elm = html`
  <h1>TEST</h1>
  <${Wrapper} />
  <${Wrapper} />
  `

  ReactDOM.render(elm, document.getElementById("App"))

</script>

さて、ここまで長かったですが、ここで議論したいのは、「このように関数を関数内に移動したということだけが、React.useCallback() を使う理由になりうるか」ということです。
確かに、関数内関数は関数が実行されるたびに定義されますので、モジュールレベルの関数に比べ処理が増えることは間違いありません。どのくらい増えるのか、有意なほど増えるかということは別にしても、モジュールレベルで定義できるならそうした方がよいのでしょう。関数の定義場所が変わることによる名前空間の混乱(ローカル変数の参照の仕方が変わってしまうことも含む)など、それはそれでデメリットもありますのでそれとの兼ね合いになるでしょうか。

<Button onClick={e => {一連の処理}} />

いまさら、このような書き方も全部だめですやめてください、いわれたら辛すぎますよね。
しかし、ここでの主題はそれではありません。真の主題は「React.useCallback() を使うことにより、モジュールレベル関数と同じほどのメリットを得られるのか」ということです。
ちなみに、React.useCallback() を使ったソースが次です。

sample6.html
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width">
  <title>Test</title>
</head>

<body>
  <div id="App"></div>
</body>

<script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>
<script crossorigin src="https://unpkg.com/htm"></script>

<script>

  // htm is JSX-like syntax in plain JavaScript - no transpiler necessary.
  // https://github.com/developit/htm
  const html = htm.bind(React.createElement)

  const Wrapper = props => {

    const sample = React.useCallback(() => {
      // 一連の処理
      let result = "H"
      result += "E" 
      result += "L" 
      result += "L" 
      result += "O"
      return result 
    }, [])

    return html`<p>${sample()}</p>`
  }

  const elm = html`
  <h1>TEST</h1>
  <${Wrapper} />
  <${Wrapper} />
  `

  ReactDOM.render(elm, document.getElementById("App"))

</script>

個人的には「美しくない」と思います。理由はソースを書いて1年くらいしたら何のために React.useCallback() を使用しているのか忘れそうだからです。結構そういう感覚って大事じゃありません?とはいっても、その点はここではまったく別の問題です。すみません。
さて、そもそも、関数が実行されるたびに関数を定義する、という処理って有意に気にすべきほどの時間を食う処理なのでしょうか。
これを解明するためには、V8 エンジンあたりの解析を行うべきなのでしょうが、やりません。面倒ですので想像だけします。想像できるストーリーとしては大きく分けて次の3つがあります。

  1. 関数が実行されるたびに関数定義がなされ、関数定義の具体的処理としての関数内関数の解析もそのたびに行われる。
  2. 関数が実行されるたびに関数定義がなされるが、関数定義の具体的処理としての関数内関数の解析は1度目の関数実行時にだけ行われ、2度目以降は、その関数定義に名前が付与される(あるいは実体化される)だけ。
  3. 関数内関数の解析もモジュールがロードされたときに1度だけ実行され、関数の実行時には、その関数定義に名前が付与される(あるいは実体化される)だけ。

もし 1 ならば React.useCallback() を使う意味がありそうです。一方、2 あるいは 3 ならば React.useCallback() を使う意味はなさそうです。「関数定義に名前が付与される(あるいは実体化される)」という処理だけなら気にするほどのコストはかからないはずだからです。それどころか、もし、2 や 3 のように、「関数の実行時には、その関数定義に名前が付与される(あるいは実体化される)だけ」であるとしたら、かえって React.useCallback() を使うことで処理が増加する可能性すらあります。
JavaScript が内部で実際にどのように処理を行うのかについては識者のコメントを待ちたいと思いますが、僕は 1 である可能性は低い気がしています。「一連の処理」自体が実行時に変化するわけではないからです。
ちなみに、Pascal という言語でも関数内関数が使え、Pascal 言語はコンパイル言語ですからロードという概念はないのですが、当然コンパイル時に関数定義の解析がなされますから 3 のタイプといえるでしょう。
とりあえず、今のところは、この議論については、これ以上深まりそうにもないので、「おそらくこの点に関しては React.useCallback() を使う理由にはならない」ということにして、次に進みたいと思います。

React.useCallback() による高速化とは、関数内関数化により発生する余計なレンダリングを防ぐことである

こちらは議論ではなく、厳然たる事実ですのできちんと勉強して理解しましょう。理解するためのポイントは2つあり、「React.memo()」と「シャロー比較」です。早速それぞれを見てみましょう。

 React.memo() とは

コンポネントが更新されると、その子コンポーネントも更新されます。次のソースを実行しボタンをクリックしてみてください。クリックするたびに、コンソール(ブラウザの F12 を押すと表示されるはず)にログが追加されるはずです。

sample7.html
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width">
  <title>Test</title>
</head>

<body>
  <div id="App"></div>
</body>

<script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>
<script crossorigin src="https://unpkg.com/htm"></script>

<script>

  // htm is JSX-like syntax in plain JavaScript - no transpiler necessary.
  // https://github.com/developit/htm
  const html = htm.bind(React.createElement)

  const MrChin = props => {

    // 一連の処理
    let result = props.m1
    result += props.m2
    result += props.m3
    result += props.m4
    result += props.m5

    console.log("Updated!: " + Date())

    return html`
    <p>${result}</p>
    `
  }

  const Wrapper = props => {

    const redrawMe = React.useState()[1]

    return html`
    <p>
      <button onClick=${() => {
        redrawMe(new Date().getTime())
      }}>
        Click!
      </button>
      <${MrChin} m1="H" m2="e" m3="l" m4="l" m5="o" />
    </p>
    `
  }

  const elm = html`
  <h1>Test</h1>
  <${Wrapper}/>
  `

  ReactDOM.render(elm, document.getElementById("App"))

</script>

ここで、MrChin コンポーネント内の「一連の処理」が、10秒くらい時間がかかる処理であったとします。ユーザーは親コンポーネントが更新されるたびに MrChin が更新されて 10秒待たされることになります。
これを回避するための機能が React.memo() です。次のソースを実行すると、React.memo() により、いくらボタンをクリックしてもコンソールにログは出力されなくなります。
React.memo() は、props に変化がない限り、更新を防止します。sample8.html では props が変化しえないため、2度と MrChin が更新されることはありません。

sample8.html
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width">
  <title>Test</title>
</head>

<body>
  <div id="App"></div>
</body>

<script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>
<script crossorigin src="https://unpkg.com/htm"></script>

<script>

  // htm is JSX-like syntax in plain JavaScript - no transpiler necessary.
  // https://github.com/developit/htm
  const html = htm.bind(React.createElement)

  const MrChin = React.memo(props => {

    // 一連の処理
    let result = props.m1
    result += props.m2
    result += props.m3
    result += props.m4
    result += props.m5

    console.log("Updated!: " + Date())

    return html`
    <p>${result}</p>
    `
  })

  const Wrapper = props => {

    const redrawMe = React.useState()[1]

    return html`
    <p>
      <button onClick=${() => {
        redrawMe(new Date().getTime())
      }}>
        Click!
      </button>
      <${MrChin} m1="H" m2="e" m3="l" m4="l" m5="o" />
    </p>
    `
  }

  const elm = html`
  <h1>Test</h1>
  <${Wrapper}/>
  `

  ReactDOM.render(elm, document.getElementById("App"))

</script>

シャロー比較とは

ここまで読み進んでこられた方であれば、これについては十分な知識を持っている方が多いと思われるため、軽めにしておきます。
シャロー比較は、JavaScript の値の比較の方法のひとつです。オブジェクト型などの変数を代入する場合の代入の仕方には、いわゆるシャローコピーとディープコピーとがありますが、変数の比較の際にも同じような問題が生じるのです。
あるいは、C 言語などでポインタの知識をお持ちの方であれば、ポインタ同士を比較するがシャロー比較で、ポインタが指しているその先のメモリの内容まで比較するのがディープ比較(というのでしょうか?)といえば理解しやすいかもしれません。
そして、React.memo() での props が変化したかどうかのチェックは、このシャロー比較で行われます。

関数とシャロー比較

前述のように、モジュールレベルの関数は、ロード時に定義される(実体化される)だけのため、シャロー比較すると、毎回「同じ値だよ」と判断されることになります。
モジュールレベルの関数を使用したソースです。React.memo() がきちんと働いている(ログが追加されない)ことが分かるかと思います。

sample9.html
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width">
  <title>Test</title>
</head>

<body>
  <div id="App"></div>
</body>

<script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>
<script crossorigin src="https://unpkg.com/htm"></script>

<script>

  // htm is JSX-like syntax in plain JavaScript - no transpiler necessary.
  // https://github.com/developit/htm
  const html = htm.bind(React.createElement)

  const MrChin = React.memo(props => {

    // 一連の処理
    let result = props.m1
    result += props.m2
    result += props.m3
    result += props.m4
    result += props.m5

    console.log("Updated!: " + Date())

    return html`
    <p>${result + " " + props.country()}</p>
    `
  })

  const Wrapper = props => {

    const redrawMe = React.useState()[1]

    return html`
    <p>
      <button onClick=${() => {
        redrawMe(new Date().getTime())
      }}>
        Click!
      </button>
      <${MrChin} m1="H" m2="e" m3="l" m4="l" m5="o" country=${sample} />
    </p>
    `
  }

  const sample = () => {
    return "Japan"
  }

  const elm = html`
  <h1>Test</h1>
  <${Wrapper}/>
  `

  ReactDOM.render(elm, document.getElementById("App"))

</script>

一方、関数内関数は、関数が実行されるたびに定義される(実体化される)ので、シャロー比較すると、毎回「違う値だよ」と判断されることになります。
関数内関数を使用したソースです。React.memo() が機能しません(ログが追加されてしまう)。

sample10.html
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width">
  <title>Test</title>
</head>

<body>
  <div id="App"></div>
</body>

<script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>
<script crossorigin src="https://unpkg.com/htm"></script>

<script>

  // htm is JSX-like syntax in plain JavaScript - no transpiler necessary.
  // https://github.com/developit/htm
  const html = htm.bind(React.createElement)

  const MrChin = React.memo(props => {

    // 一連の処理
    let result = props.m1
    result += props.m2
    result += props.m3
    result += props.m4
    result += props.m5

    console.log("Updated!: " + Date())

    return html`
    <p>${result + " " + props.country()}</p>
    `
  })

  const Wrapper = props => {

    const redrawMe = React.useState()[1]

    const sample = () => {
      return "Japan"
    }

    return html`
    <p>
      <button onClick=${() => {
        redrawMe(new Date().getTime())
      }}>
        Click!
      </button>
      <${MrChin} m1="H" m2="e" m3="l" m4="l" m5="o" country=${sample} />
    </p>
    `
  }

  const elm = html`
  <h1>Test</h1>
  <${Wrapper}/>
  `

  ReactDOM.render(elm, document.getElementById("App"))

</script>

この関数内関数の対シャロー比較問題を解決するのが React.useCallback() です。次のソースです。見事に解決し、ログが追加されなくなりました。

sample11.html
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width">
  <title>Test</title>
</head>

<body>
  <div id="App"></div>
</body>

<script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>
<script crossorigin src="https://unpkg.com/htm"></script>

<script>

  // htm is JSX-like syntax in plain JavaScript - no transpiler necessary.
  // https://github.com/developit/htm
  const html = htm.bind(React.createElement)

  const MrChin = React.memo(props => {

    // 一連の処理
    let result = props.m1
    result += props.m2
    result += props.m3
    result += props.m4
    result += props.m5

    console.log("Updated!: " + Date())

    return html`
    <p>${result + " " + props.country()}</p>
    `
  })

  const Wrapper = props => {

    const redrawMe = React.useState()[1]

    const sample = React.useCallback(() => {
      return "Japan"
    }, [])

    return html`
    <p>
      <button onClick=${() => {
        redrawMe(new Date().getTime())
      }}>
        Click!
      </button>
      <${MrChin} m1="H" m2="e" m3="l" m4="l" m5="o" country=${sample} />
    </p>
    `
  }

  const elm = html`
  <h1>Test</h1>
  <${Wrapper}/>
  `

  ReactDOM.render(elm, document.getElementById("App"))

</script>

このように、React.useCallback() は、関数内関数がシャロー比較において「違う値だよ」と判断されてしまう問題を解決します。この点については異論がありません。
シャロー比較は、わりといろいろな場面で使われます。例えば、React.useState() の戻り値である、setState() の引数での同一性チェックや、React.useEffect の「依存リスト引数」での同一性チェックなどです。しっかりマスターしましょう。

ここまでは理解した。でも、useCallback() の「依存リスト引数」ってどうよ

React.useCallback() の使いどころを理解できたとしても、「依存リスト引数」についてきちんと理解していなければ、使えるはずなどありません。「依存リスト引数」は React.useCallback の2番目の引数です。

React.useCallback(関数, 依存リスト)

「依存している」って何なのか

多くの文献を読ませていただきましたが、多くは「依存している変数等を指定してあげる」くらいの記述しかありませんでした。
これだけでは、僕ごときの知能ではまるで理解ができません。
関数内部で使用している変数をとりあえず全部並べておけ的な文献もありましたのでとりあえず実験してみることにしました。
まずは、モジュールレベルの変数を利用した場合に、「依存リスト引数」として渡す必要があるのかを確認します。ソースです。

sample12.html
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width">
  <title>Test</title>
</head>

<body>
  <div id="App"></div>
</body>

<script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>
<script crossorigin src="https://unpkg.com/htm"></script>

<script>

  // htm is JSX-like syntax in plain JavaScript - no transpiler necessary.
  // https://github.com/developit/htm
  const html = htm.bind(React.createElement)

  let now = "?"

  const Clicker = props => {
    return html`
    <p>
      <button onClick=${() => {
        now = " " + Date()
        props.updater()
      }}>
        Click!
      </button>
      <p>${now}</p>
    </p>
    `
  }

  const Wrapper1 = props => {

    const redrawMe = React.useState()[1]

    const updater = () => {
      redrawMe(new Date().getTime())
      now = "今は " + now
      console.log(now)
    }

    return html`
    <${Clicker} updater=${updater} />
    `
  }

  const Wrapper2 = props => {

    const redrawMe = React.useState()[1]

    const updater = React.useCallback(() => {
      redrawMe(new Date().getTime())
      now = "今は " + now
      console.log(now)
    }, [])

    return html`
    <${Clicker} updater=${updater} />
    `
  }

  const elm = html`
  <h1>Test</h1>
  <${Wrapper1}/>
  <${Wrapper2}/>
  `

  ReactDOM.render(elm, document.getElementById("App"))

</script>

Wrapper1 では、関数内関数をそのまま使用し、Wrapper2 では React.useCallback() を使用しましたが、結果は同じでした。
すなわち、モジュールレベルの変数を内部で使用している場合には必ず「依存リスト引数」にそれを指定せよ、とまではいえないということになります。
次に、関数内関数の引数を利用した場合に、「依存リスト引数」として渡す必要があるのかを確認します。ソースです。

sample13.html
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width">
  <title>Test</title>
</head>

<body>
  <div id="App"></div>
</body>

<script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>
<script crossorigin src="https://unpkg.com/htm"></script>

<script>

  // htm is JSX-like syntax in plain JavaScript - no transpiler necessary.
  // https://github.com/developit/htm
  const html = htm.bind(React.createElement)

  let now = "?"

  const Clicker = props => {
    return html`
    <p>
      <button onClick=${() => {
        now = " " + Date()
        props.updater()
      }}>
        Click!
      </button>
      <p>${now}</p>
    </p>
    `
  }

  const Wrapper1 = props => {

    const redrawMe = React.useState()[1]

    const updater = (n) => {
      redrawMe(new Date().getTime())
      n = "今は " + n
      console.log(n)
    }

    return html`
    <${Clicker} updater=${updater} />
    `
  }

  const Wrapper2 = props => {

    const redrawMe = React.useState(now)[1]

    const updater = React.useCallback((n) => {
      redrawMe(new Date().getTime())
      n = "今は " + n
      console.log(n)
    }, [])

    return html`
    <${Clicker} updater=${updater} />
    `
  }

  const elm = html`
  <h1>Test</h1>
  <${Wrapper1}/>
  <${Wrapper2}/>
  `

  ReactDOM.render(elm, document.getElementById("App"))

</script>

やはり、Wrapper1 では、関数内関数をそのまま使用し、Wrapper2 では React.useCallback() を使用しましたが、結果は同じでした。
すなわち、引数を必ず「依存リスト引数」にも指定せよ、とまではいえないということになります。
では次に、関数内のローカル変数を利用した場合に、「依存リスト引数」として渡す必要があるのかを確認します。ソースです。

sample14.html
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width">
  <title>Test</title>
</head>

<body>
  <div id="App"></div>
</body>

<script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>
<script crossorigin src="https://unpkg.com/htm"></script>

<script>

  // htm is JSX-like syntax in plain JavaScript - no transpiler necessary.
  // https://github.com/developit/htm
  const html = htm.bind(React.createElement)

  let now = ["?"]

  const Clicker = props => {
    return html`
    <p>
      <button onClick=${() => {
        props.setter(" " + Date())
        props.updater()
      }}>
        Click!
      </button>
      <p>${props.getter()}</p>
    </p>
    `
  }

  const Wrapper1 = props => {

    const redrawMe = React.useState()[1]

    const localNow = now

    const getter = () => {
      return localNow[0]
    }

    const setter = v => {
      localNow[0] = v
    }

    const updater = () => {
      redrawMe(new Date().getTime())
      localNow[0] = "今は " + localNow[0]
      console.log(localNow[0])
    }

    return html`
    <${Clicker} getter=${getter} setter=${setter} updater=${updater} />
    `
  }

  const Wrapper2 = props => {

    const redrawMe = React.useState()[1]

    const localNow = now

    const getter = React.useCallback(() => {
      return localNow[0]
    }, [])

    const setter = React.useCallback(v => {
      localNow[0] = v
    }, [])

    const updater = React.useCallback(() => {
      redrawMe(new Date().getTime())
      localNow[0] = "今は " + localNow[0]
      console.log(localNow[0])
    }, [])

    return html`
    <${Clicker} getter=${getter} setter=${setter} updater=${updater} />
    `
  }

  const elm = html`
  <h1>Test</h1>
  <${Wrapper1}/>
  <${Wrapper2}/>
  `

  ReactDOM.render(elm, document.getElementById("App"))

</script>

やはり、Wrapper1 では、関数内関数をそのまま使用し、Wrapper2 では React.useCallback() を使用しましたが、結果は同じでした。
すなわち、関数内のローカル変数を内部で使用している場合には必ず「依存リスト引数」にそれを指定せよ、とまではいえないということになります。
ここで、ニヤリとされた方はかなり熟練のプログラマーです。まだ気づけないルーキープログラマも、sample14.html と次の sample15.html とを比較すれば。薄々何かを感じ始めるかもしれません。

sample15.html
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width">
  <title>Test</title>
</head>

<body>
  <div id="App"></div>
</body>

<script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>
<script crossorigin src="https://unpkg.com/htm"></script>

<script>

  // htm is JSX-like syntax in plain JavaScript - no transpiler necessary.
  // https://github.com/developit/htm
  const html = htm.bind(React.createElement)

  const Clicker = props => {
    return html`
    <p>
      <button onClick=${() => {
        props.setter(" " + Date())
        props.updater()
      }}>
        Click!
      </button>
      <p>${props.getter()}</p>
    </p>
    `
  }

  const Wrapper1 = props => {

    let now = "?"

    const redrawMe = React.useState()[1]

    const getter = () => {
      return now
    }

    const setter = v => {
      now = v
    }

    const updater = () => {
      redrawMe(new Date().getTime())
      now = "今は " + now
      console.log(now)
    }

    return html`
    <${Clicker} getter=${getter} setter=${setter} updater=${updater} />
    `
  }

  const Wrapper2 = props => {

    let now = "?"

    const redrawMe = React.useState()[1]

    const getter =  React.useCallback(() => {
      return now
    }, [])

    const setter =  React.useCallback(v => {
      now = v
    }, [])

    const updater = React.useCallback(() => {
      redrawMe(new Date().getTime())
      now = "今は " + now
      console.log(now)
    }, [])

    return html`
    <${Clicker} getter=${getter} setter=${setter} updater=${updater} />
    `
  }

  const elm = html`
  <h1>Test</h1>
  <${Wrapper1}/>
  <${Wrapper2}/>
  `

  ReactDOM.render(elm, document.getElementById("App"))

</script>

みなさん、ついてこれてますでしょうか。
Wrrapper1 は動作しません。関数内のローカル変数は、関数が実行されるたびに、すなわち、更新処理が発生するたびに初期化されますから、ずっと ? のままです。
なぜこのようなことになったのでしょうか。sample14.html と sample.15.html を比較しましょう。
そうです、sample14.html では、ローカル変数が、モジュールレベルの変数への参照なのでうまくいったのです。ここでもやはりシャローやディープといったものが関係してくるのです。
すなわち。ここでは、ローカル変数 localNow に代入されるモジュールレベルの変数 now は配列型です。配列型やオブジェクト型などの代入はシャローコピーされます。参照渡しともいいます。
C 言語をやっている方なら、配列型やオブジェクト型のようなメモリを多く消費する型の変数をポインタで処理するのと同じ、と考えれば理解できるはずです。
スクリプト言語ではポインタを使えない(ポインタの概念は捨てたほうが言語として素敵と考えている)ものが多いですが、シャローとか参照渡しとかいう概念が残るなら、その基礎にあるポインタの概念も知っておいた方が理解度アップには有利な気がします。
ちなみに、props 引数はオブジェクト型ですから参照渡しのようにも思われます。しかしながら、JSX 記法からは明らかですが、一度バラされますから、元の props との同一性がなく、うまくいきません。

sample16.html
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width">
  <title>Test</title>
</head>

<body>
  <div id="App"></div>
</body>

<script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>
<script crossorigin src="https://unpkg.com/htm"></script>

<script>

  // htm is JSX-like syntax in plain JavaScript - no transpiler necessary.
  // https://github.com/developit/htm
  const html = htm.bind(React.createElement)

  let now = {now: "?"}

  const Clicker = props => {
    return html`
    <p>
      <button onClick=${() => {
        props.now = " " + Date()
        props.updater()
      }}>
        Click!
      </button>
      <p>${props.now}</p>
    </p>
    `
  }

  const Wrapper1 = props => {

    const redrawMe = React.useState()[1]

    const updater = () => {
      redrawMe(new Date().getTime())
      props.now = "今は " + props.now
      console.log(props.now)
    }

    return html`
    <${Clicker} ...${props} updater=${updater} />
    `
  }

  const Wrapper2 = props => {

    const redrawMe = React.useState()[1]

    const updater = React.useCallback(() => {
      redrawMe(new Date().getTime())
      props.now = "今は " + props.now
      console.log(props.now)
    }, [])

    return html`
    <${Clicker} ...${props} updater=${updater} />
    `
  }

  const elm = html`
  <h1>Test</h1>
  <${Wrapper1} ...${now} />
  <${Wrapper2} ...${now} />
  `

  ReactDOM.render(elm, document.getElementById("App"))

</script>

そして、次のソースのように、JSX 記法を使わなくても、やはりうまくいきませんので、記法の問題ではなく、React 内部で props のディープコピー的な処理を行っているのだろうと思います。

sample17.html
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width">
  <title>Test</title>
</head>

<body>
  <div id="App"></div>
</body>

<script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>
<script crossorigin src="https://unpkg.com/htm"></script>

<script>

  // htm is JSX-like syntax in plain JavaScript - no transpiler necessary.
  // https://github.com/developit/htm
  const html = htm.bind(React.createElement)

  let now = {now: "?"}

  const Clicker = props => {
    return React.createElement('p', {}, [
      React.createElement('button', {'onClick': () => {
        props.now = " " + Date()
        props.updater()
      }}, [
        'Click!'
      ]),
      React.createElement('p', {}, [props.now])
    ])
  }

  const Wrapper1 = props => {

    const redrawMe = React.useState()[1]

    const updater = () => {
      redrawMe(new Date().getTime())
      props.now = "今は " + props.now
      console.log(props.now)
    }

    props.updater = updater
    return React.createElement(Clicker, props)
  }

  const Wrapper2 = props => {

    const redrawMe = React.useState()[1]

    const updater = React.useCallback(() => {
      redrawMe(new Date().getTime())
      props.now = "今は " + props.now
      console.log(props.now)
    }, [])

    props.updater = updater
    return React.createElement(Clicker, props)
  }

  const elm = React.createElement(React.Fragment, {}, [
    React.createElement('h1', {}, ['Test']),
    React.createElement(Wrapper1, now),
    React.createElement(Wrapper2, now),
  ])

  ReactDOM.render(elm, document.getElementById("App"))

</script>

なお、props のメンバをオブジェクト型にすれば、それはもちろん参照渡しになりますのでうまく動きます。

sample18.html
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width">
  <title>Test</title>
</head>

<body>
  <div id="App"></div>
</body>

<script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>
<script crossorigin src="https://unpkg.com/htm"></script>

<script>

  // htm is JSX-like syntax in plain JavaScript - no transpiler necessary.
  // https://github.com/developit/htm
  const html = htm.bind(React.createElement)

  let now = {current: "?"}

  const Clicker = props => {
    return html`
    <p>
      <button onClick=${() => {
        props.now.current = " " + Date()
        props.updater()
      }}>
        Click!
      </button>
      <p>${props.now.current}</p>
    </p>
    `
  }

  const Wrapper1 = props => {

    const redrawMe = React.useState()[1]

    const updater = () => {
      redrawMe(new Date().getTime())
      props.now.now = "今は " + props.now.current
      console.log(props.now.current)
    }

    return html`
    <${Clicker} now=${props.now} updater=${updater} />
    `
  }

  const Wrapper2 = props => {

    const redrawMe = React.useState()[1]

    const updater = React.useCallback(() => {
      redrawMe(new Date().getTime())
      props.now.now = "今は " + props.now.current
      console.log(props.now.current)
    }, [])

    return html`
    <${Clicker} now=${props.now} updater=${updater} />
    `
  }

  const elm = html`
  <h1>Test</h1>
  <${Wrapper1} now=${now} />
  <${Wrapper2} now=${now} />
  `

  ReactDOM.render(elm, document.getElementById("App"))

</script>

sample18.html により、props を関数内関数の内部で使用している場合には必ず「依存リスト引数」にそれを指定せよ、とまではいえないことも明らかになりました。
ところで、驚くべき sample15.html の Wrapper2 へ少し話を戻しましょう。これ、きちんと動作しています!
あたかも 関数ローカルな変数localNowが関数の外側に追い出されて、モジュールレベルの変数やクラスのメンバ変数になったかのようです。なぜこうなったのかについては自習をしてください。ここではこれ以上深追いしません。
深追いしない理由は、これが React.useCallback() の正しい使い方であるという自信を僕は今のところ持てないからです。識者の意見をお待ちしております。

で、「依存リスト引数」はいつ使うの?

さて、ここまで、「依存リスト引数」を使う例は出てきていません。
ただ、少なくとも、関数内で使う変数のスコープのみと関係性があるものではないことは確認できました。
ここで、「いや、関数内のローカル変数の型と、それがシャローコピーかそうでないかにより、依存リストに加えるべきかどうかが決まるのではないか」という意見はあると思います。確かにその通りなんですが、それが分かったところで実戦で役に立つとは思えないのですよね。「面倒だから全部入れとけ」派の台頭を防げない気がします。
あらためて基本に立ち返ってみましょう。そもそも僕らは何のために React.useCallBack() を使おうとしているのでしたっけ?ここでは React.memo() を働かせるためですよね。つまり、props に「思いがけない変動」(関数内関数の弊害による変動)が生じないようにするためでした。
「依存リスト引数」は、「思いがけない変動」を「想定された変動」にしようというものですが、果たして設計として正しい方向性なのでしょうか?
「想定された変動」なら、props を操作するのが正当なやりかたなのではないでしょうか?
つまり、僕らは、「関数内関数で生じた弊害」に対応するために React.useCallback() を使い始めましたが、そうしたら今度は、「React.useCallback() による弊害」が現れたので、それに対応するために「依存リスト引数」を使おうとしていて、「props を操作するのが正当だと思うけど仕方ないよね」と無理やり納得しようとしているだけなのではないか、という疑念が浮かんでくるのです。

React.useCallback() における「依存リスト引数」についてのまとめ

結局、React.useCallback() における「依存リスト引数」については、その使いどころがよくわからないままです。
個人的には、「React.useCallback() における「依存リスト引数」には、使いどころはなく、全部[]でいい。[] 以外が必要となるようなアルゴリズム自体の方を見直せ。」という結論でよい気がしています。

React.useMemo() における「依存リスト引数」

とはいっても、「依存リスト引数」は、React.useCallback() がその基本形である React.useMemo() から引き継いだものですから、React.useMemo() における「依存リスト引数」についても見ておきましょう。

※ React.memo() と React.useMemo()、全然別ものなのに名前が似ていて紛らわしいですねぇ。僕は、カンファレンスとかのビデオとかを見て React の開発陣を尊敬しているのですが、このネーミングセンスだけはいただけないですね。「メモって単語いけてない?」みたいなのりで付けただけの感じがいただけないです。将来的にはどちらかの関数名が変更されるんじゃないでしょうか。

メモ化

React.useMemo() は、「値」をメモ化するものです。

React.useMemo(「値」を返す関数, 依存リスト)

もしかしたら、「メモ化」という用語を聞きなれない方がいらっしゃるかもしれませんが、「キャッシング処理」とか「キャッシュする」などと同じと考えてよいと思います。
「値」をキャッシュする、このような処理は、経験豊かなプログラマなら何度か書いたことがある処理でありさほど難しいものではありませんよね。
なぜ、「メモ化」なんていう大袈裟?な専門用語を持ち出すのかというと、おそらく、関数内で、関数の外側の変数の存在を意識しない形でキャッシング処理を行うのは特別なものである、と考えているからでしょう。関数型言語由来かもしれません。
ようは、キャッシュの持ち方が独特なだけで、特別なものではありません。

キャッシュしたくなるのはどんな時?

キャッシュ処理を行いたくなる時って、どんなときでしょうか。
「値」をキャッシュに保持することにより、処理を高速化したいときですよね。
例えば、「値をファイルから読み込む処理があるが、毎回ファイルから読み込むと処理時間がかかるので、ファイルが更新されていなければ値をキャッシュから読み込む」というようなものが考えらます。
そして、「ファイルが更新されてい」るかどうか、という判断こそが「依存リスト引数」にあたるものです。とってもわかりやすいですね。
気を付けるべきところは、「ファイルが更新されているかどうか」というチェックの処理時間が、ファイルから値を読み込む処理時間よりも長いなら、キャッシュ処理はしない方がよいことになることです。
メモ化においても、値をメモから読み出せば足りるか、そうではなく新ためて元のリソースから読み出すべきか、という判断がきちんとできないのなら、使用するべきではありません。
すなわち、キャッシュ処理やメモ化は、安易に導入すべきものではなく、慎重に設計や実験をしたうえで導入すべきものなのです。
最後に、メモ化なんてさほど大袈裟なものではない、ということは示すためにも、できるだけ実践的なサンプルソースを掲載いたします。
url が変化したときだけ fetch し直すという、ありがちな処理を React.useMemo() と、 React Suspense を用いて作成してみました。

sample99.html
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width">
  <title>Test</title>
</head>

<body>
  <div id="App"></div>
</body>

<script crossorigin src="https://unpkg.com/react@17/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@17/umd/react-dom.production.min.js"></script>
<script crossorigin src="https://unpkg.com/htm"></script>

<script>

  // htm is JSX-like syntax in plain JavaScript - no transpiler necessary.
  // https://github.com/developit/htm
  const html = htm.bind(React.createElement)

  // Render-as-You-Fetch
  // https://reactjs.org/docs/concurrent-mode-suspense.html#approach-3-render-as-you-fetch-using-suspense
  const wrapPromise = promise => {
    let status = "pending";
    let result;
    const suspender = promise.then(
      r => {
        status = "success";
        result = r;
      },
      e => {
        status = "error";
        result = e;
      }
    );
    return {
      read() {
        if (status === "pending") {
          throw suspender;
        } else if (status === "error") {
          throw result;
        } else if (status === "success") {
          return result;
        }
      }
    };
  }

  // 指定されたミリ秒待つ関数
  const sleep = msec => new Promise(resolve => setTimeout(resolve, msec))

  const FromRestApiInner = props => {
    return props.resource.read()
  }

  const FromRestApi = props => {

    const getText = async (url) => {
      try {
        const res = await fetch(url,
         //{mode: 'cors', credentials: 'include'}, 
        )
        await sleep(500) // 効果を分かりやすくするためにあえて 500ms ウエィト
        const json = await res.json()
        return html`
          <p><ul>${
            json.data.children.map((c, i) => html`
              <li key=${i}>${c.data.title}</li>
            `)
          }</ul></p>
        `
      } catch (e) {
        return e.message
      }
    }

    // url が変化したときだけ fetch し直す。React.useMemo() の威力です。
    const resource = React.useMemo(() =>
     wrapPromise(getText(`https://www.reddit.com/r/${props.url}.json`))
     , [props.url]
    )

    return html`
    <${React.Suspense} fallback=${html`<p>Now Loading...</p>`}>
      <${FromRestApiInner} resource=${resource}/>
    <//>
    `
  }

  const UserInput = props => {

    const redrawMe = React.useState()[1]
    const refSelect = React.useRef({value: "reactjs"}) // ここではお手軽な uncontroled component を採用しました

    return html`
    <p>
      <select name="url" style=${{marginRight:"1rem"}} ref=${refSelect}>
        <option value="reactjs">reactjs</option>
        <option value="frontend">frontend</option>
      </select>
      <button onClick=${() => redrawMe(new Date().getTime())}>
        Reload Page
      </button>
      <${FromRestApi} url=${refSelect.current.value}/>
    </p>
    `
  }

  const elm = html`
  <h1>Test</h1>
  <${UserInput}/>
  `

  ReactDOM.render(elm, document.getElementById("App"))

</script>

このサンプルを作成して思ったのですが、React.useMemo() を使いこなせば、React.memo() は「いらない子」になっていく気がしました。React.memo() が「いらない子」なら、ほぼそのためだけに存在するといってよいっぽい React.useCallback() もだんだん「いらない子」に近づいてくような・・・

React.useCallback() アゲイン

キャッシュ処理における更新チェック、と考えれば、React.useCallback() を React.memo() とは無関係に、React.useMemo() の単なるショートハンドとして使うときには、「依存リスト引数」を使うこともあるのかな?
いや、でもそもそも単なるショートハンドとしての使い道すらなさそうな。だって、更新チェックが必要なら、関数を生成する過程も必要ですよね。次のソースように。

React.useMemo(() => {

  // 何らかの、関数生成に影響を与える一連の処理、スタート
  :
  :
  // 何らかの、関数生成に影響を与える一連の処理、ここまで

  return 生成した関数

}, [関数生成の依存リスト])

闇は深そうです。

最後に

いかがでしたでしょうか。
この記事が少しでも React プログラマーの役に立てれば幸せです。
識者のコメントをお待ちしております。

4
6
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
6