今日はReact Hooksの中の一つ、useCallbackを整理していきます。
Contents
そもそもuseCallbackとは何か?
useCallbackがやることは、同じ処理をする関数ならば、元の関数と同じものとして扱うと良い時もあるので、その機能を提供するものです。イメージ的には関数のメモ化です。それだけです。
JavaScriptでの関数は、結局はオブジェクトです。例えば、アロー関数はJavaScriptの仕様として常に新規の関数オブジェクトを生成するが(つまり処理としては同じでも異なる関数オブジェクトであると言う意味。以下コード例参照)
useCallbackは「処理的に同じ関数」が返るかどうかを判別して、同じ処理をする関数が返るべきならば、前に渡した関数を再利用します。
function add() {
return (a, b) => a + b;
}
const sum1 = add();
const sum2 = add();
/* 同じ処理をする関数だが、異なるオブジェクトなので、当然false. */
console.log(sum1 === sum2); // false
console.log(sum1 === sum1); // true
上記の簡単なコード例のように、同じ値を返す関数なのに、異なる関数オブジェクト(sum1とsum2)をpropsに渡すと無駄なコンポーネントのレンダリングが生じるので、useCallbackを使うことで、それらを同じ関数とみなし、無駄なレンダリングを防ぐことができますよ!と言うことです。
以下、Reactのコード例と共に整理します。
useCallbackの使い所
useCallbackの主要な使い所は以下の2ケースです。
- React.memoを使う時(後ほどコード例と共に整理します)
- 関数オブジェクトが、他のhooksに依存している場合。
1については、後ほどコード例と共に詳しく整理していきます。
2ですが、例えば、下記のようにuseEffectを使う場合に、あるcallbackに依存していたとします。先ほど説明したように、全く同じ処理をする関数であったとしても、関数オブジェクトとして異なるものであるとみなされ、このuseEffect内のコードは毎回実行されてしまいますので、callbackをuseCallbackでラップしてあげましょう。
useEffect(
() => {
// 省略
},
[callback],
);
useCallbackを使わないコード例(よろしくない例)
ボタンを押せば、カウントが+1されるだけのコード。
import React, { useState } from "react";
import ReactDOM from "react-dom";
import { Hello } from "./Hello";
const App = () => {
const [count, setCount] = useState(0);
return (
<div>
{/* Appコンポーネントのレンダー毎に、異なる関数オブジェクトが生成される。 */}
<Hello increment={() => setCount(count + 1)} />
<div>count: {count}</div>
</div>
);
};
ReactDOM.render(<App />, document.getElementById("container"));
import React, { useRef } from "react";
// 現在、helloボタンが押される度に、このHelloコンポーネントはレンダーされている。
// 本当にアップデートする必要があるのは、ページに表示しているカウントのナンバーだけである。
// このコンポーネントは関係ない。
export const Hello = React.memo(({ increment }) => {
const renders = useRef(0);
console.log("renders: ", renders.current++);
return <button onClick={increment}>hello</button>;
});
ここで、React.memoですが、React.memoで関数をラップすることにより、props(この例ではincrementの部分)に変更があったときだけ、そのコンポーネントをレンダーするように制御できます。
しかし!
以下のように、Helloコンポーネントは、ボタンを押す度にレンダーされている。あまり、よろしくないです。(厳密に言えば、インライン関数は低コストだし、この例のHelloコンポーネントのようなシンプルなものでは特に問題になりません。と言うのは脇に置いておきましょう。)
useCallbackを使い無駄なコンポーネントレンダリングをなくしたコード例
Hello.jsに変更はないので、省略します。変更を加えたのは以下のコードのみです。
import React, { useState, useCallback } from "react";
import ReactDOM from "react-dom";
import { Hello } from "./Hello";
const App = () => {
const [count, setCount] = useState(0);
// incrementは、同じ関数オブジェクトとなる。
const increment = useCallback(() => {
setCount((prevCount) => prevCount + 1);
}, [setCount]);
return (
<div>
<Hello increment={increment} />
<div>count: {count}</div>
</div>
);
};
ReactDOM.render(<App />, document.getElementById("container"));
ボタンを9回押しても、Helloコンポーネントは初期の一度しかレンダーされていません。increment関数をuseCallbackでメモ化することにより(依存配列に変更がない限りは)、同じ関数オブジェクトを返すようになるので、Helloコンポーネントはリレンダーされなくなったと、言うわけです。
ポイントはこの部分です。
const increment = useCallback(() => {
setCount((prevCount) => prevCount + 1);
}, [setCount]);
- useCallbackを使う(incrementのアロー関数をuseCallbackに渡す)
- count変数を使うのではなく、更新関数(以前のステートの値を利用できる)を使ってcountを更新している
1は、まあuseCallbackを使えと言う話ですが、注意すべきは2です。元のようにcount変数をパスすると以下のようなコードになります。
useCallbackを使っているが、意味のない例が以下です。
const increment = useCallback(() => {
setCount(count + 1);
}, [setCount, count]);
useCallbackの第二引数には、依存配列を渡しますので、当然ながら、依存しているsetCount, countを共に渡す必要が出てきます。これだと、countは、ボタンをクリックする毎に、更新されていくので、increment関数も再生成され、helloコンポーネントは、ボタンを押す度にレンダーされます。ということで、prevStateのように更新関数を使いましょう。
useCallbackのまとめ
useCallbackをサクッとまとめると、useCallbackは同じ処理をする関数オブジェクトを同じものとみなす機能を提供してくれる。主な使い所は、React.memoを使いコンポーネントのレンダリングを減らしたい時や、他のHooksを使う時に、コールバック関数が依存している時に、使うべし。