6
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2021-01-25

フックuseMemouseCallbackは、コンポーネントの再描画のたびに値を再計算したり、関数インスタンスがつくられるのを避け、必要なときだけ処理するテクニックです。いわばキャッシュのような仕組みで、「メモ化」と呼ばれます。

前に書いた記事「React: コンポーネントのロジックをカスタムフックに切り出す ー カウンターの作例で」のサンプル001として、CodeSandboxに掲げたカウンターの作例を使ってご説明します。本稿で解説する修正を加えたサンプルコードも、CodeSandboxに公開しました。

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

カウンターの数字の色を正負で変える

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

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
2101001_002.png

useMemoフックで値をメモ化する

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

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

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

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

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を使ったカウンター表示のモジュール

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と同じ依存配列を渡します。

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

useCallbackと同じ処理を、useMemoでつぎのように書くこともできます。この構文を見やすく書けるようにしたのが、useCallbackです。次項では、あえてこの回りくどい構文を使ってみます。

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

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

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())の構文について補っておきましょう。引数に値を渡すほかに、「関数型の更新」という書き方があります。引数の関数は、現在の状態値を引数に受け取り、戻り値が新たな値になるのです。

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

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

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

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

src/useCounter.js
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に公開したサンプルコードでお確かめください。

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でコールバック関数を条件によって無効にする

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 };
};
  1. useMemouseCallbackの最適化の観点からの解説としては、「雰囲気で使わない React hooks の useCallback/useMemo」や「React.memo / useCallback / useMemo の使い方、使い所を理解してパフォーマンス最適化をする」が参考になるでしょう。

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

6
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?