React: コールバックを条件によって無効にする ー useCallbackをあえてuseMemoで書き替えた例

フック[`useMemo`](https://ja.reactjs.org/docs/hooks-reference.html#usememo)と[`useCallback`](https://ja.reactjs.org/docs/hooks-reference.html#usecallback)は、コンポーネントの再描画のたびに値を再計算したり、関数インスタンスがつくられるのを避け、必要なときだけ処理するテクニックです。いわばキャッシュのような仕組みで、「メモ化」と呼ばれます。

前に書いた記事「[React: コンポーネントのロジックをカスタムフックに切り出す ー カウンターの作例で](https://qiita.com/FumioNonaka/items/62285e474608388cbbe3)」の[サンプル001](https://codesandbox.io/s/react-creating-custom-hook-cxps9)として、CodeSandboxに掲げたカウンターの作例を使ってご説明します。本稿で解説する修正を加えたサンプルコードも、[CodeSandboxに公開](https://codesandbox.io/s/react-using-usememo-and-usecallback-hooks-2y8of)しました。

メモ化は、つねに最適化につながるとはかぎりません[^1]。とくにこのサンプルのように単純なコードではなおさらです。本稿は、フック`useMemo`と`useCallback`の使い方についてご理解いただくことを主眼とします。

[^1]: `useMemo`と`useCallback`の最適化の観点からの解説としては、「[雰囲気で使わない React hooks の useCallback/useMemo](https://qiita.com/seya/items/8291f53576097fc1c52a)」や「[React.memo / useCallback / useMemo の使い方、使い所を理解してパフォーマンス最適化をする](https://qiita.com/soarflat/items/b9d3d17b8ab1f5dbfed2)」が参考になるでしょう。

# カウンターの数字の色を正負で変える
まずは、カウンターの数字がマイナスになったとき、色を赤くしてみましょう。メモ化は使わないつぎのコードで、数字の正負によってカラーが変わります(図001)。

```react:src/CounterDisplay.js
const CounterDisplay = ({ counter }) => {
	return (
		<div>

			{/* <span>{counter.count}</span> */}
			<span style={{color: counter.count < 0 ? 'red' : 'black'}}>{counter.count}</span>

		</div>
	);
}
```

#### 図001■カウンターの数字の色が正負で変わる
![2101001_001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/128949/04a245a2-ab46-ef3d-e691-439a2d565d17.png)
![2101001_002.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/128949/cc940030-a902-2908-723a-c4b9a4e523ef.png)


# useMemoフックで値をメモ化する
`useMemo`フックには、引数がふたつあります。第1引数は関数で、戻り値がメモ化された値です。関数本体には、値を算出するための処理が書かれます。第2引数は配列で、要素は変更があったとき再計算(依存)すべき変数です。

```react
const メモ化された値 = useMemo(算出関数, [依存配列])
```

第2引数の依存配列は、構文上は省けます。けれど、その場合コンポーネントがレンダーされるたびに再処理されますので、フックを使う意味がありません。適切な変数を加えてください。

前掲のカウンター表示のモジュールで`useMemo`を使うには、つぎのコードのように書き替えます。モジュールの記述全体は、以下のコード001のとおりです。依存配列に加えたカウンター(`counter.count`)の値が変わると、再計算されます。

```react:src/CounterDisplay.js
// 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を使ったカウンター表示のモジュール
```react:src/CounterDisplay.js
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`と同じ依存配列を渡します。

```react
const メモ化された関数 = useCallback(コールバック関数, [依存配列])
```

[`useCallback`と同じ処理](https://ja.reactjs.org/docs/hooks-reference.html#usecallback)を、`useMemo`でつぎのように書くこともできます。この構文を見やすく書けるようにしたのが、`useCallback`です。次項では、あえてこの回りくどい構文を使ってみます。

```react
const コールバック関数 = useMemo(() => コールバック関数, [依存配列])
```

カスタムフックのモジュールで、カウンター減算と加算のコールバック関数は`useCallback`フックでつぎのように書き直せます。依存配列に加えたカウンター(`count`)の値が変わったら、関数は生成し直されるということです。

```react:src/useCounter.js
// 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()`)の構文について補っておきましょう。引数に値を渡すほかに、「[関数型の更新](https://ja.reactjs.org/docs/hooks-reference.html#functional-updates)」という書き方があります。引数の関数は、現在の状態値を引数に受け取り、戻り値が新たな値になるのです。

`useCallback`で書き替えた前掲のコードで、関数型の更新を用いれば、依存変数はなくせます。依存なしの場合には、第2引数に空の配列`[]`を渡してください。すると、コンポーネントがはじめて描画されたときのみコールバック関数がつくられることになり、無駄な再生成が省けるのです。

```react:src/useCounter.js
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で止めましょう。コールバックに、つぎのように条件を加えれば済むことです。

```react:src/useCounter.js
// const decrement = useCallback(() => setCount((_count) => _count - 1), []);
const decrement = useCallback(() => {
	if (count < -4) { return; }
	setCount((_count) => _count - 1);
// }, []);
}, [count]);
```

でも、減算ボタンをクリックしたときの、コールバック関数の呼び出しは止まりません。コールバックを無効にしてしまえないでしょうか[^2]。コードをつぎのように書き替えることが考えられます。

```react:src/useCounter.js
const decrement = useCallback(
	count < -4 ? null : () => setCount((_count) => _count - 1)
, [count]);
```

[^2]: 今回の例であれば、減算ボタンに`disabled`属性を定めれば、`onClick`ハンドラを無効にできます。ただ、本稿では`useMemo`と`useCallback`を軸に考えましょう。ご参考までに、CodeSandboxのサンプルコードには、コメントアウトして`disabled`を使った記述が添えてあります。

けれど、`useCallback`の第1引数には、関数を定めることになっています。`null`という値は関数として解釈されないのです。

> React Hook useCallback received a function whose dependencies are unknown. Pass an inline function instead.

`useMemo`の算出関数は、値が返せました。そして、関数も値に含まれます。つまり、`useMemo`を用いれば、つぎのように書き替えられるのです。書き直したカスタムフックのモジュールの記述は、以下のコード002にまとめました。各モジュールのコードと、実際の動きは[CodeSandboxに公開](https://codesandbox.io/s/react-using-usememo-and-usecallback-hooks-2y8of)したサンプルコードでお確かめください。

```react:src/useCounter.js
// 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でコールバック関数を条件によって無効にする
```react:src/useCounter.js
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 };
};
```