フックuseMemo
とuseCallback
は、コンポーネントの再描画のたびに値を再計算したり、関数インスタンスがつくられるのを避け、必要なときだけ処理するテクニックです。いわばキャッシュのような仕組みで、「メモ化」と呼ばれます。
前に書いた記事「React: コンポーネントのロジックをカスタムフックに切り出す ー カウンターの作例で」のサンプル001として、CodeSandboxに掲げたカウンターの作例を使ってご説明します。本稿で解説する修正を加えたサンプルコードも、CodeSandboxに公開しました。
メモ化は、つねに最適化につながるとはかぎりません1。とくにこのサンプルのように単純なコードではなおさらです。本稿は、フックuseMemo
とuseCallback
の使い方についてご理解いただくことを主眼とします。
カウンターの数字の色を正負で変える
まずは、カウンターの数字がマイナスになったとき、色を赤くしてみましょう。メモ化は使わないつぎのコードで、数字の正負によってカラーが変わります(図001)。
const CounterDisplay = ({ counter }) => {
return (
<div>
{/* <span>{counter.count}</span> */}
<span style={{color: counter.count < 0 ? 'red' : 'black'}}>{counter.count}</span>
</div>
);
}
図001■カウンターの数字の色が正負で変わる
useMemoフックで値をメモ化する
useMemo
フックには、引数がふたつあります。第1引数は関数で、戻り値がメモ化された値です。関数本体には、値を算出するための処理が書かれます。第2引数は配列で、要素は変更があったとき再計算(依存)すべき変数です。
const メモ化された値 = useMemo(算出関数, [依存配列])
第2引数の依存配列は、構文上は省けます。けれど、その場合コンポーネントがレンダーされるたびに再処理されますので、フックを使う意味がありません。適切な変数を加えてください。
前掲のカウンター表示のモジュールでuseMemo
を使うには、つぎのコードのように書き替えます。モジュールの記述全体は、以下のコード001のとおりです。依存配列に加えたカウンター(counter.count
)の値が変わると、再計算されます。
// import React from "react";
import React, {useMemo} from "react";
const CounterDisplay = ({ counter }) => {
const color = useMemo(
() => counter.count < 0 ? 'red' : 'black'
, [counter.count]
);
return (
<div>
{/* <span style={{color: counter.count < 0 ? 'red' : 'black'}}>{counter.count}</span> */}
<span style={{color: color}}>{counter.count}</span>
</div>
);
}
コード001■useMemoを使ったカウンター表示のモジュール
import React, {useMemo} from "react";
const CounterDisplay = ({ counter }) => {
const color = useMemo(
() => counter.count < 0 ? 'red' : 'black'
, [counter.count]
);
return (
<div>
<button onClick={counter.decrement}>-</button>
<span style={{color: color}}>{counter.count}</span>
<button onClick={counter.increment}>+</button>
</div>
);
}
export default CounterDisplay;
useCallbackフックでコールバック関数をメモ化する
useCallback
フックは、処理する関数をメモ化します。useMemo
と似た構文で、違いは第1引数が呼び出すコールバック関数だということです。第2引数には、useMemo
と同じ依存配列を渡します。
const メモ化された関数 = useCallback(コールバック関数, [依存配列])
useCallback
と同じ処理を、useMemo
でつぎのように書くこともできます。この構文を見やすく書けるようにしたのが、useCallback
です。次項では、あえてこの回りくどい構文を使ってみます。
const コールバック関数 = useMemo(() => コールバック関数, [依存配列])
カスタムフックのモジュールで、カウンター減算と加算のコールバック関数はuseCallback
フックでつぎのように書き直せます。依存配列に加えたカウンター(count
)の値が変わったら、関数は生成し直されるということです。
// import { useState } from 'react';
import { useCallback, useState } from 'react';
export const useCounter = (initialCount = 0) => {
// const decrement = () => setCount(count - 1);
const decrement = useCallback(() => setCount(count - 1), [count]);
// const increment = () => setCount(count + 1);
const increment = useCallback(() => setCount(count + 1), [count]);
};
ここで、状態(state
)設定関数(useState()
)の構文について補っておきましょう。引数に値を渡すほかに、「関数型の更新」という書き方があります。引数の関数は、現在の状態値を引数に受け取り、戻り値が新たな値になるのです。
useCallback
で書き替えた前掲のコードで、関数型の更新を用いれば、依存変数はなくせます。依存なしの場合には、第2引数に空の配列[]
を渡してください。すると、コンポーネントがはじめて描画されたときのみコールバック関数がつくられることになり、無駄な再生成が省けるのです。
export const useCounter = (initialCount = 0) => {
const [count, setCount] = useState(initialCount);
// const decrement = useCallback(() => setCount(count - 1), [count]);
const decrement = useCallback(() => setCount((_count) => _count - 1), []);
// const increment = useCallback(() => setCount(count + 1), [count]);
const increment = useCallback(() => setCount((_count) => _count + 1), []);
return { count, decrement, increment };
};
条件によってコールバックを無効にする
ここで、カウンターの減算を-5で止めましょう。コールバックに、つぎのように条件を加えれば済むことです。
// const decrement = useCallback(() => setCount((_count) => _count - 1), []);
const decrement = useCallback(() => {
if (count < -4) { return; }
setCount((_count) => _count - 1);
// }, []);
}, [count]);
でも、減算ボタンをクリックしたときの、コールバック関数の呼び出しは止まりません。コールバックを無効にしてしまえないでしょうか2。コードをつぎのように書き替えることが考えられます。
const decrement = useCallback(
count < -4 ? null : () => setCount((_count) => _count - 1)
, [count]);
けれど、useCallback
の第1引数には、関数を定めることになっています。null
という値は関数として解釈されないのです。
React Hook useCallback received a function whose dependencies are unknown. Pass an inline function instead.
useMemo
の算出関数は、値が返せました。そして、関数も値に含まれます。つまり、useMemo
を用いれば、つぎのように書き替えられるのです。書き直したカスタムフックのモジュールの記述は、以下のコード002にまとめました。各モジュールのコードと、実際の動きはCodeSandboxに公開したサンプルコードでお確かめください。
// import { useCallback, useState } from 'react';
import { useCallback, useMemo, useState } from 'react';
export const useCounter = (initialCount = 0) => {
// const decrement = useCallback(() => {
const decrement = useMemo(() =>
// if (count < -4) { return; }
(count < -4) ? null
// setCount((_count) => _count - 1);
: () => setCount((_count) => _count - 1)
// }, [count]);
, [count]);
};
コード002■useMemoでコールバック関数を条件によって無効にする
import { useCallback, useMemo, useState } from 'react';
export const useCounter = (initialCount = 0) => {
const [count, setCount] = useState(initialCount);
const decrement = useMemo(() =>
(count < -4) ? null
: () => setCount((_count) => _count - 1)
, [count]);
const increment = useCallback(() => setCount((_count) => _count + 1), []);
return { count, decrement, increment };
};
-
useMemo
とuseCallback
の最適化の観点からの解説としては、「雰囲気で使わない React hooks の useCallback/useMemo」や「React.memo / useCallback / useMemo の使い方、使い所を理解してパフォーマンス最適化をする」が参考になるでしょう。 ↩ -
今回の例であれば、減算ボタンに
disabled
属性を定めれば、onClick
ハンドラを無効にできます。ただ、本稿ではuseMemo
とuseCallback
を軸に考えましょう。ご参考までに、CodeSandboxのサンプルコードには、コメントアウトしてdisabled
を使った記述が添えてあります。 ↩