こんにちは。 @nerikosans です。みなさん、React Hooksしていらっしゃいますでしょうか。
今日は僕がHooksで詰まって3時間溶かして学んだことのメモを書いていこうと思います。
悲劇
例えば arrow function があったとして、それをhooksのdepsに入れてしまうと各render毎に異なるオブジェクトとして認識されてしまうのは日本書紀にも記述がありますね。
つまり
const getMySuperBenriFunction = () => {
return (a: number, b: number) => a + b;
}
const benri1 = getMySuperBenriFunction();
const benri2 = getMySuperBenriFunction();
console.log(benri1 === benri2) // => false
であり、
import getMySuperBenriFunction from './getMySuperBenriFunction';
const MySupremeComponent: React.FC = () => {
const benri = getMySuperBenriFunction();
const [v1, setV1] = React.useState(1);
const [v2, setV2] = React.useState(3);
// v1, v2が変わると出力
// benriが変化することは想定してない
React.useEffect(() => {
console.log(benri(v1, v2));
}, [v1, v2, benri]);
return (
<div>
(ここにすごいUI)
</div>
)
}
みたいなことをすると全renderで useEffect
が走って非常につらい気持ちになります。
デバッグ策
さて、前述のarrow functionの件はもちろんヤマトタケルの時代から決まっているのですが、うっかりこれを失念してしまったとしましょう。
そんなとき、「なぜこの useEffect
が走るのかわからない...どのdepsが変化しているんや...」という調査を比較的ラクにできる手法を紹介します。
つまり、変数の値を前サイクルと比較できればよいのです。
ということでご登場願いましょう、usePrevious
さーん!
usePreviousさん
function usePrevious<T>(value: T) {
const ref = React.useRef<T>();
React.useEffect(() => {
ref.current = value;
});
return ref.current;
}
usePreviousさんの勇姿
function usePrevious<T>(value: T) { /*...*/ }
const MySupremeComponent: React.FC = () => {
/* ... */
console.log(usePrevious(benri) === benri) // => false
React.useEffect(() => {
console.log(benri(v1, v2));
}, [v1, v2, benri]);
return (
{ /*...*/ }
)
}
ということで、 usePrevious
さんを用いて前サイクルと比較すれば、どのdepsの変化でEffectが発火しているのか、比較的早く発見できるでしょう。最高!
usePreviousさんの出自
なお、御大はReact公式FAQで紹介されています。公式hookになる可能性もあるみたいです。
おまけ
hooksプロのみなさんには言うまでもないかと思いますが、この問題の解決策は
const getMySuperBenriFunction = () => {
return React.useCallback((a: number, b: number) => a + b, []);
}
と benri
自体をcallbackに入れることですね。