こんにちは、ミナトです。
今回はReact HooksのuseCallbackについて解説します。
useCallbackとは?
useCallbackはパフォーマンス最適化を行うためのフックです。
useCallbackを利用すると依存する変数が変化した場合以外はメモ化(保存された)されたコールバック関数を返されます。
引数に指定した値が変更された場合のみ関数が変更されます。
毎回コンポーネントがレンダリングされる度に関数の定義が行われるとパフォーマンスを低下させることがあります。
useCallbackを利用して、依存する値以外が変更された場合は、メモ化された値を返すことで、パフォーマンスが最適化されます。
useCallbackの使い方
const 変数名 = useCallback(関数, [依存する変数...]);
# 例
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
useCallbackの第一引数には、メモ化する対象の関数を指定します。
第二引数には、配列の形式で依存する値を指定します。useCallbackで生成された関数は第二引数に指定した値が変更された場合のみ再表示されます。
第二引数に空の配列([])を指定した場合はコンポーネントの初回レンダリング時のみ関数が定義されます。
また、第二引数を指定しない場合は、毎回再表示されます。
useCallbackの実装例
今回はボタンクリックした際に値をカウントアップして表示する簡単なアプリを例に動作を確認します。
React Appの作成
まずはReactのアプリを新規作成します。作成方法については以下の記事を参考にしてください。
コンポーネントの作成
まずは、最適化を行わない状態で動作を確認します。
componetを格納するディレクトリを作成します。(
)my-app/src/components/use_callback
の中にCount.jsとCountButton.jsを作成します。my-app/src/components/use_callback
Count.jsはcount1、count2のstateを表示するコンポーネント
CountButton.jsはcount1、count2のstateを変更するためのイベントハンドラを実行するボタン
最適化利用しないパターン
まずはApp.jsを以下のように修正します。
- useStateでcount1とcount2を定義
- stateを変更する関数addCount1とaddCount2を定義
- count1用のCount.jsとCountButton.jsを表示
Countのpropsにはnameとcountを渡します。
CountButtonにはpropsのhandleClickにクリック時のイベントハンドラを渡します。また、「Add Count 1」の部分はコンポーネント内でchildrenで受け取れます。 - 同様にcount2用のCount.jsとCountButton.jsを表示
import {useState} from 'react';
import './App.css';
import Count from './components/use_callback/Count';
import CountButton from './components/use_callback/CountButton';
function App() {
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);
const addCount1 = () => {
setCount1(prev => prev + 1);
};
const addCount2 = () => {
setCount2(prev => prev + 1);
};
return (
<div className="App">
<header className="App-header">
<Count name="count1" count={count1} />
<CountButton handleClick={addCount1}>Add Count1</CountButton>
<Count name="count2" count={count2} />
<CountButton handleClick={addCount2}>Add Count2</CountButton>
</header>
</div>
);
}
export default App;
次にCount.jsを以下のように修正します。
Count.jsはpropsで受け取った値を表示するだけの単純なコンポーネントです。
コンポーネントがレンダリングされたことが分かるようにconsol.logでログを出力しておきます。
import React from 'react';
const Count = ({name, count}) => {
console.log(`Count: ${name}`)
return (
<div>
{count}
</div>
);
};
export default Count;
次にCountButton.jsを以下のように修正します。
CountButton.jsではpropsで受け取ったhandleClickをボタンをクリックイベントハンドラとしてセットしています。ボタンをクリックすると対応するstateがカウントアップされます。
こちらも同様にコンポーネントがレンダリングされたことが分かるようにconsol.logでログを出力しておきます。
import React from 'react';
const CountButton = ({handleClick, children}) => {
console.log('CountButton', children)
return (
<div>
<button onClick={handleClick}>{children}</button>
</div>
);
};
export default CountButton;
この状態でアプリを起動して動作を確認してみてください。
初回レンダリング時に定義した4つのCountとCountButtonコンポーネントが全てレンダリングされ、コンソールにログが出力されます。
また、どちらかのボタンをクリックした際も全てのコンポーネントが全て再レンダリングされているのが確認できるかと思います。
count1のstateを変更した場合それに関連するコンポーネントだけがレンダリングされば良いのですが、このままだと関係ないものまで全てのコンポーネントが再レンダリングされてしまい、パフォーマンスがよくない状態です。
この問題を防ぐために、useCallbackとReact.memoを利用することができます。
React.memoを利用して最適化
Count.jsに渡しているname、countのpropsやCountButton.jsのchildrenはReact.memoを利用することで最適化できます。
Count.jsとCountButton.jsのexportの部分を以下のように変更していください。
export default React.memo(Count);
export default React.memo(CountButton);
この状態で再度アプリを起動して動作を確認してください。
ボタンをクリックした際にログを確認するとCountコンポーネントの方がクリックした方のみ表示されているのが確認できると思います。(CountButtonの方はまだ両方出てる)
React.memoではReactの要素を記録しておき、レンダリング時に内容をチェックして変更があった場合のみ再レンダリングしてくれています。
しかし、まだ関数に対してはメモ化できておらず、未だ複数レンダリングされてしまっています。そこでこれを防ぐためにuseCallbackを利用します。
React.memo + useCallbackを利用して最適化
App.jsを以下のように修正します。
まずはuseCallbackをインポートします。
そして、addCount1とaddCount2にuseCallbackを適用します。useCallbackの第二引数には依存する変数を指定します。
こうしておくことで第二引数に指定した値が変化した場合のみ関数がレンダリングされます。
import {useState, useCallback} from 'react'; // ここを修正
import './App.css';
import Count from './components/use_callback/Count';
import CountButton from './components/use_callback/CountButton';
function App() {
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);
const addCount1 = useCallback(() => {
setCount1(prev => prev + 1);
}, [count1]);
const addCount2 = useCallback(() => {
setCount2(prev => prev + 1);
}, [count2]);
return (
<div className="App">
<header className="App-header">
<Count name="count1" count={count1} />
<CountButton handleClick={addCount1}>Add Count1</CountButton>
<Count name="count2" count={count2} />
<CountButton handleClick={addCount2}>Add Count2</CountButton>
</header>
</div>
);
}
export default App;
アプリを起動してください。
今回はcount1が変更された場合のみ、addCount1が再レンダリングされます。また、addCount2はcount2が変更された場合のみ再レンダリングされます。
useCallbackを利用することで関数もメモ化することができました。
まとめ
- useCallbackを利用することで不必要な関数のレンダリングを避け、パフォーマンスを最適化できる
- useMemoの第一引数には対象の関数を渡す
- useMemoの第二引数には依存する変数を配列形式で渡す
最後まで読んでいただき、ありがとうございます。
この記事が、「面白いな」、「勉強になったな」という方は、SNSでシェアしていただけると嬉しいです。