Posted at

React Hooksで非同期処理を仕掛ける場合、refが便利

React Hooksを使っていくと、今までの感覚ではうまくいかないことがよくあります。


大前提:関数内の関数はクロージャを形成する

ReactではなくJavaScriptレベルの話ですが、関数内のローカル変数は関数の実行ごとに生成されます。そして、関数Aの内で別な関数Bを作成して、Bの中からAのローカル変数を参照すると、Aの実行によって生成された変数が、Bから使うためにAの終了後も生き残ることとなります。これが「クロージャ」です。


React Hooksの場合

React Hooksを使う場合、忘れてはならないのは、コンポーネントの描画ごとに関数が再実行されることです。ただ単にコンポーネント内で書いた関数ももちろんクロージャになりますが、コンポーネントの描画(=再実行)ごとに関数が再生成されますので、そのままReact内のイベントハンドラなどにセットしている場合は値の束縛の問題は生じません。


非同期処理に入る

これが問題となるのが、「useCallbackなどでコールバックをキャッシュさせる場合」や「setTimeoutfetchなど、React外の非同期処理に繋げる場合」です。前者については以前に触れましたが、破棄用の配列を適切に指定すれば問題なく乗り切れます。しかし、後者の場合はそうもいきません。

function SimpleTimer(){

const [value, setValue] = React.useState(1);

React.useEffect(() => {
if(value % 5 !== 0) return;
setTimeout(() => alert(value), 2000);
}, [value])

return(
<div>
<button type="button" onClick={() => setValue(num => num-1)}>-</button>
{value}
<button type="button" onClick={() => setValue(num => num+1)}>+</button>
</div>
);
}

ものすごく人工的な例ですが、「値が5の倍数になってから2秒後」にsetTimeoutの関数が実行されます。ただ、valuesetTimeoutを仕掛けた時点のものを束縛してしまっているので、タイマーが動いている間にvalueを変えても、それは読み取ってくれません。


mutableな値の置き場としてのref

ここで登場するのがuseRefです。refという名前の通り、「コンポーネントへの参照」を入れるという印象も強いですが、単なる値の置き場としても利用可能で、Hooks内部ではインスタンス変数の代わりとしても利用可能です(React FAQ)。

では、今回の「コールバックの束縛」に対策を行ってみましょう。

  const ref = React.useRef(null);

ref.current = () => alert(value);
React.useEffect(() => ref.current = null, [ref]);
const mutableCallback = React.useCallback(() => {
const func = ref.current;
func && func();
}, [ref]);

まず、refを作成して、次にref.currentに関数を代入しますが、この代入は毎回実行されます。そして、コールバックの実行時refから関数を取り出してそれを実行すれば、どんなタイミングで実行したとしても、直前にref.currentへ代入した、最新の値に束縛された関数が実行される、という寸法です。

なお、React.useEffectは、アンマウント後にコールバックが呼ばれて意図しない動作をするのを防ぐため、コールバックの動作を止めるためのものです。

このようにして作ったmutableCallbacksetTimeoutの引数にセットすれば、最新のvalueを拾うようになります。

非同期コールバックをよく使うのであれば、useMutableCallbackのように切り出しておいてもいいかもしれません。


CodePenでの実装例


See the Pen
jJYbEv
by Jkr2255 (@jkr2255)
on CodePen.