【React.js】useCallbackとuseMemoの使い方をご紹介(React hooksを学ぶ)
目次
今回も、前回に引き続きreact hooksについて記事をまとめていきたいと思います。
これまでuseState、useEffect、useContext、useReducerを学んできました。
過去の記事は下記からご覧ください。
【React.js】useStateとuseEffectの使い方をご紹介(React hooksを学ぶ)
https://www.dailyupblog.com/web_development/1888/
【React.js】useContext、useReducerの使い方をご紹介(React hooksを学ぶ)
https://www.dailyupblog.com/web_development/1926/
今回は、関数や値における不要な再計算をスキップできるuseCallbackとuseMemoの二つのフックについてご紹介します。
それではいきましょう!
Memo化とは?
Memo化とは、計算結果を保持して、それを再利用する手法のことです。
例えば、同じ結果を返すような処理に対して、初回のみ処理を実行し、結果を保持しておき、値が必要となった際に、処理を実行することなく、保持した値を呼び出し利用するような機能です。
つまり、キャッシュのような機能です。
このMemo化を行うことによって、無駄な処理をスキップすることができ、パフォーマンス向上につながります。
実際の例を見てみましょう。
「components」フォルダ内にtestMemo.tsxファイルを作成しましょう。
そのファイルの中に下記のように記述してください。
import React, { useState } from 'react';
type CountButton = {
value: number,
handleClick: () => void
}
type CountText = {
value: number
}
const Message = () => {
console.log("こんにちは");
return <h1>こんにちは</h1>
}
const Text = ({value}: CountText) => {
return <p>{value}</p>
}
const Button = ({value,handleClick}: CountButton) => {
console.log(value);
return (
<button onClick={handleClick}>+1</button>
)
}
const TestMemo = () => {
const [count, setCount] = useState(0);
const incrementCount = () => setCount(count + 1);
return (
<>
<Message />
<Text value={count} />
<Button value={count} handleClick={incrementCount} />
</>
)
}
export default TestMemo;
上記の例では、3つのコンポーネントを呼び出しています。
このコンポーネントにはそれぞれ役割があり、「Message」コンポーネントは、「こんにちは」という文字列をコンソールに出力しつつ、h1タイトルとして出力します。
「Text」コンポーネントは、stateの値を出力し、「Button」コンポーネントはクリックすることで、stateの値を更新するためのボタンを出力します。
useStateで「count」というstateを用意し、incrementCount関数は、このstateに1を足すというものになります。
この状態で、一旦保存し、ブラウザで挙動を確認してみましょう。
※ちなみに、index.tsxでこのtextMemoコンポーネントを呼び出す際に、コンポーネントの呼び出しタグを囲っている「<React.StrictMode>」というタグは外しましょう。
今回、コンソールを確認することがありますが、このタグがついていると、コンポーネントが2回呼び出されてしまいます。
これは、コンポーネントのバグや潜在的なミスを洗い出すために2回呼び出す機能で、reactにこのようにデフォルトでつくようになっています。
今回は、逆にこれがあることでわかりにくくなりかねないので、一旦外しましょう。
ブラウザで下記のように表示されると思いますが、ボタンをクリックするたびにコンソールに表示がされると思います。
値が更新されているcountStateの値はいいのですが、値などが特に変わらない「こんにちは」も毎回のレンダリング時に呼び出されてしまっています。
これは不要にコンポーネントが呼び出されていることになるので、早速Memo化の機能を使ってみます。
testMemoを下記のように書き換えてください。
//・・・省略
const Message = React.memo(() => {
console.log("こんにちは");
return <h1>こんにちは</h1>
})
const Text = React.memo(({value}: CountText) => {
return <p>{value}</p>
});
const Button = React.memo(({value,handleClick}: CountButton) => {
console.log(value);
return (
<button onClick={handleClick}>+1</button>
)
})
//・・・省略
上記のように、3つのコンポーネントを「React.memo」を使って、Memo化します。
これで再度ブラウザで挙動を確認してみてください。
下記のように、「こんにちは」は初回のみ出力され、それ以降のレンダリング時には出力がされていないことが確認できると思います。
よって、Messageコンポーネントは、初回のみ呼び出されているということになります。
この機能がMemo化になります。
useCallbackについて
前述のMemo化を踏まえた上で、useCallbackを紹介いたします。
useCallbackは指定した依存配列に含まれる値が変化した時のみにMemo化した値を再計算するフックです。
今回紹介するuseCallbackは関数自体をMemo化することが特徴です。
それでは実際の例を見てみます。
「components」フォルダ内にUseCallback.tsxファイルを作成し、一旦下記のように記述してください。
import React, { useState } from 'react';
type RandomValue = {
handleClick: () => void,
value: string | number,
text: string
}
const Title = React.memo(() => {
console.log("useCallBackテスト");
return <h1>useCallBackテスト</h1>
})
const Button = React.memo(({handleClick, value, text}: RandomValue) => {
console.log(value);
return <button onClick={handleClick}>{text}</button>
})
const UseCallback = () => {
const str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
const randStr = Array.from(Array(10)).map(()=>str[Math.floor(Math.random()*str.length)]).join('')
const [strRandState, setStrRandState] = useState("");
const [numRandState, setNumRandState] = useState(0);
const outputRandStr = () => setStrRandState(randStr);
const outputRandNum = () => setNumRandState(Math.random());
return (
<>
<Title />
<Button handleClick={outputRandStr} value={strRandState} text="ランダムな文字列を出力" />
<Button handleClick={outputRandNum} value={numRandState} text="乱数を出力" />
</>
)
}
export default UseCallback;
上記の例では、ランダムな10桁の文字列を出力するボタンと、乱数を出力するボタンの二つのボタンを設置しています。
このボタンは同じコンポーネントを利用していますが、ButtonコンポーネントはMemo化しています。
これを一旦ブラウザで表示して、動作を確認してみましょう。
下記のように表示されると思うので、「ランダムな文字列を出力」と「乱数を出力」のボタンをそれぞれ1回ずつ押してみましょう。
すると、コンソール上には下記のように、ランダムな文字列出力ボタンを押した場合でも乱数も一緒に出力され、乱数出力ボタンを押した場合でもランダムな文字列も一緒に出力されてしまっています。
つまり、Memo化したはずなのに、更新されていない方のstateに関しても再生成されてしまっています。
これは、関数が返す結果が同じでも、親コンポーネントであるUseCallbackコンポーネントが再レンダーされたタイミングで、関数自体が再生成されてしまっており、React.memoが別の関数として認識してしまったことにより発生する現象です。
これを防ぐためにReact.memoと合わせてuseCallbackを使います。
先ほどのUseCallback.tsxを下記にように書き換えてください。
//・・・省略
const UseCallback = () => {
const str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
const randStr = Array.from(Array(10)).map(()=>str[Math.floor(Math.random()*str.length)]).join('')
const [strRandState, setStrRandState] = useState("");
const [numRandState, setNumRandState] = useState(0);
const outputRandStr = useCallback(() => setStrRandState(randStr), [strRandState]) //useCallback
const outputRandNum = useCallback(() => setNumRandState(Math.random()), [numRandState]) //useCallback
return (
<>
<Title />
<Button handleClick={outputRandStr} value={strRandState} text="ランダムな文字列を出力" />
<Button handleClick={outputRandNum} value={numRandState} text="乱数を出力" />
</>
)
//・・・省略
上記のように、outputRandStr関数とoutputRandNum関数それぞれ、関数そのものをuseCallbackによってMemo化することで、不要な関数の再生成を防ぐことができます。
useCallback関数には、第一引数に関数、第二引数に依存配列を指定します。
この依存配列に指定した値に変化があった場合にのみ、メモ化した値の再計算を行います。
これで再度ブラウザで確認してみてください。
下記のように、二つのボタンをそれぞれ1回ずつ押してみましたが、押したボタンに対応する値しか出力されていないことがわかります。
useMemoについて
useMemoは、計算の結果を保持するフックです。
何度やっても同じ結果の値を保持し、そこから値を再取得します。
useMemoに関しても、依存配列で値を指定し、その値が変更された場合にのみ、再計算をします。
useCallbackとの違いとしては、useCallbackは関数自体をMemo化するのに対して、useMemoは関数の結果そのものをMemo化します。
関数の結果をMemo化することで、パフォーマンスの向上が見込めます。
例を見てみましょう。
「components」ディレクトリにUseMemo.tsxファイルを作成してください。
import { useMemo, useState } from 'react';
const UseMemo = () => {
const [count01, setCount01] = useState(0)
const [count02, setCount02] = useState(0)
const result01 = () => setCount01(count01 + 1)
const result02 = () => setCount02(count02 + 1)
const dubleCount = useMemo(() => {
return count01 * 2;
}, [count01])
return (
<>
<p>Count01: {count01}</p>
<p>Count02: {count02}</p>
<p>DubleCount: {dubleCount}</p>
<button onClick={result01}>count01 + 1</button>
<button onClick={result02}>count02 + 1</button>
</>
)
}
export default UseMemo;
上記の例では、「count01」と「count02」といった二つのstateを用意し、それぞれの値に1を足すといった関数も用意します。
さらに、今回はcount01が更新された場合にのみcount01を2倍にする値を返す処理の結果をuseMemoを用いてMemo化しています。
二つのボタンを設置して、これらのボタンをそれぞれクリックすることで、関数が実行され、それぞれの値が変更されるといったものになります。
実際このコードがどのようになるのか、挙動を確認してみましょう。
ブラウザでアクセスして、二つのボタンをそれぞれ押してみてください。
count02に1をプラスするボタンを押すと、count02が更新されるだけですが、count01に1をプラスするボタンを押すと、Memo化したcount01の再計算が行われ、2倍された値がページ上の「DubleCount:」の後に表示されたと思います。
このように、依存配列に指定した値をMemo化し、この値に変更が加わった時のみに、再計算を実行することで無駄な再レンダーを防ぐことができました。
最後に
今回は、Memo化を行うことでパフォーマンスを向上させる二つのフック、useCallbackとuseMemoを紹介しました。
ただ、注意点としては、これら二つのフックの使い所です。
必ずしも、Memo化することがパフォーマンス向上につながるとは限りません。
それは、Memo化にもコストがかかるため、Memo化にかかるコストを大幅に上回るような大規模な処理などに使うことで、これらフックは真価を発揮するでしょう。
なので、闇雲に使うのではなく、使い所を見極めてうまく活用していきたいところです。
前回:【React.js】useContext、useReducerの使い方をご紹介(React hooksを学ぶ)
https://www.dailyupblog.com/web_development/1926/
次回:【React.js】useRefとuseImperativeHandleの使い方をご紹介(React hooksを学ぶ)
https://www.dailyupblog.com/web_development/1978/