React + TypeScript: use-debounceでコードの繰り返し実行を間引く

同じコードが短い時間に繰り返し実行されるとき、処理を間引きたい場合があります。そのような制御([debounce](https://ejje.weblio.jp/content/debounce))を行うライブラリが[`use-debounce`](https://github.com/xnimorz/use-debounce#readme)です。

* 間引き処理に特化しているため、容量は1KBにも及びません。
* 使い方が標準的で、実装は[Underscore](https://underscorejs.org/)や[Lodash](https://lodash.com/)と同じです。
* サーバーレンダリングにも対応しています。

間引きの処理が必要な例として本稿で採り上げるお題は、公式サイトを参考にしたつぎのふたつです。

* テキストフィールドに入力される値の変更を間引く。
    - 値をAPIにリクエストするような場合に送信の負荷を下げる。
* 画面がスクロールされるときに再描画を減らす。
    - スクロールする範囲が大きい場合に逐次描画し直さない。


# インストール
インストールは、npmまたはyarnでつぎのように行ってください。

```terminal
npm install --save use-debounce
```

```terminal
yarn add use-debounce
```


# `useDebounce`で値の変更を間引く
まずは、間引きをしないテキストフィールドへの入力のコード例です。`<input>`要素にひと文字入力するたびに、`onChange`ハンドラで状態変数(`text`)が更新されます。値は画面に「Actual value」(`<p>`要素)として示しました。

```react:src/Input.tsx
import { useState } from 'react';
import type { FC } from 'react';

export const Input: FC = () => {
	const [text, setText] = useState('Hello');
	return (
		<div>
			<input
				onChange={({ target: { value } }) => {
					setText(value);
				}}
			/>
			<p>Actual value: {text}</p>
		</div>
	);
};
```

`use-debounce`の中でもっともシンプルな関数が[`useDebounce`](https://github.com/xnimorz/use-debounce#simple-values-debouncing)です。変更を監視する値(`value`)とその間隔のミリ秒(`delay`)を引数に渡します。戻り値は配列で、第1要素(`state`)が間引き後の値です。

```react
const [state] = useDebounce(value, delay);
```

**引数**
* `value`: 変更を監視する値[^1]。
* `delay`: 監視間隔のミリ秒。

**戻り値**
* `[state]`: 間引き後の状態値を第1要素にもつ配列。

`useDebounce`関数には、テキスト入力フィールドの状態変数(`text`)と監視間隔(`1000`)を渡します。戻り値から得られる間引き後の値(`value`)は、画面に「Debounce value:」(`<p>`要素)として加えました。テキストフィールドの値が、入力のたびに変わることは同じです。けれど、間引き後の値は、入力を指定間隔止めなければ変更されません。以下のサンプル001に掲げたのはCodeSandboxに公開した作例です。動きや各モジュールのコードについては、こちらをご参照ください。

```react:src/Input.tsx
import { useDebounce } from 'use-debounce';

export const Input: FC = () => {

	const [value] = useDebounce(text, 1000);
	return (
		<div>
			<input
				defaultValue={"Hello"}
    
			/>

			<p>Debounce value: {value}</p>
		</div>
	);
};
```

#### サンプル001■React + TypeScript: useDebounce example with use-debounce
https://codesandbox.io/embed/react-typescript-usedebounce-example-with-use-debounce-frsczk?fontsize=14&hidenavigation=1&theme=dark

[^1]: 値が変更されたかどうかを確かめるのは、[デフォルトでは単純な等価比較](https://github.com/xnimorz/use-debounce/blob/master/src/useDebounce.ts)(`===`)です。そのため、対象がオブジェクトのときは、`useDebouncedCallback`でプロパティ値を取り出して調べなければなりません(`useDebounce`の第3引数にオプションオブジェクトで比較関数を渡すことはできます)。


# `useDebouncedCallback`で関数の呼び出しを間引く
単純な値の変更ではなく、コールバック関数の呼び出しを間引くのが[`useDebouncedCallback`](https://github.com/xnimorz/use-debounce#debounced-callbacks)です。第1引数にコールバック(`func`)、第2引数には間引く呼び出し間隔(`wait`)を渡してください。戻り値(`debounced`)が呼び出しの間引かれる新たな関数です。

```react
const debounced = useDebouncedCallback(func, wait);
```

**引数**
* `func`: 呼び出しを間引くコールバック関数。
* `wait`: 呼び出しを間引く間隔のミリ秒。

**戻り値**
* `debounced`: 呼び出しが間引かれる新たなコールバック関数。

もとにするコードは、前項と同じ冒頭のテキスト入力フィールドの例にしましょう。呼び出しが間引かれるコールバック関数(`debounced`)は、`onChange`ハンドラに定めます。すると、ハンドラ関数が状態変数(`text`)を書き替えますので、間引かれたあとの値は別の状態変数にとっておく必要はありません(前掲サンプル001の変数`value`)。

```react:src/Input.tsx
import { useDebouncedCallback } from 'use-debounce';

export const Input: FC = () => {

	const debounced: ChangeEventHandler<HTMLInputElement> = useDebouncedCallback(
		({ target: { value } }) => {
			setText(value);
		},
		1000,
	);
	return (
		<div>

			<input
				defaultValue={"Hello"}
				/* onChange={({ target: { value } }) => {
					setText(value);
				}} */
				onChange={debounced}
			/>
			{/* <p>Actual value: {text}</p> */}
			<p>Debounce value: {text}</p>
		</div>
	);
};
```

これで、状態変数(`text`)は間引かれたあとの値(「Debounce value」)になりました(サンプル002)。テキストフィールド(`<input>`要素)の表示はキー入力するたびに変わるので、これが実際の値(「Actual value」)です。

#### サンプル002■React + TypeScript: useDebouncedCallback example 01 with use-debounce
https://codesandbox.io/embed/react-typescript-usedebouncedcallback-example-01-with-use-debounce-2jdxhr?fontsize=14&hidenavigation=1&theme=dark


# `useDebouncedCallback`でスクロール画面の再描画を抑える
本稿のふたつめのお題です。スクロール画面の再描画を`useDebouncedCallback`で抑えてみましょう。画面の`scroll`イベントは、ライブラリ[Subscribe-event](https://github.com/xnimorz/subscribe-event#readme)で捉えることにします。少し古いものの、扱いが簡単で小さなライブラリです。npmまたはyarnでインストールしてください。

```terminal
npm install --save subscribe-event
```

```terminal
yarn add subscribe-event
```

イベントを購読する`subscribe`関数の構文はつぎのとおりです。戻り値(`unsubscribe`)が購読終了の関数であることにご注意ください。

```react
const unsubscribe = subscribe(element, event, eventCallback);
```

**引数**
* `element`: イベントが引き起こされる要素。
* `event`: 購読するイベント。
* `eventCallback`: イベントのコールバック関数。

**戻り値**
* `unsubscribe`: イベントの購読を破棄する関数。

先に、`useDebouncedCallback`は使わない画面のスクロールです。[`window`の`scroll`イベント](https://codingls.com/js/1001/)に応じてコールバック関数を呼び出します。ここでは、`useEffect`に定めました。関数は単に[`window.pageYOffset`](https://developer.mozilla.org/docs/Web/API/Window/scrollY)の値を状態変数(`position`)に与えているだけです。コンポーネントアンマウント時の購読破棄(`unsubscribe`)は、`useEffect`の戻り値であるクリーンアップ関数から呼び出します。

状態変数値(`position`)は、確認できるよう画面のテキスト「Debounced top position」(`<p>`要素)に示しました。もうひとつ数値を与えたのが、`ref`オブジェクト(`updatedCount`)です(初期値`0`)。コンポーネント(`ScrolledComponent`)が描画されるたびにカウントアップされます。この再描画回数(`updatedCount.current`)も、テキスト「Component rerendered <INS>nnn</INS> times」(`<p>`要素)として画面に加えました。スクロールを続けていると、回数はどんどん増えることが確かめられるでしょう。

```react:src/ScrolledComponent.tsx
import { useEffect, useRef, useState } from 'react';
import type { FC } from 'react';
import subscribe from 'subscribe-event';

export const ScrolledComponent: FC = () => {
	const updatedCount = useRef(0);
	updatedCount.current++;
	const [position, setPosition] = useState(window.pageYOffset);
	useEffect(() => {
		const unsubscribe = subscribe(window, 'scroll', () => {
			setPosition(window.pageYOffset);
		});
		return () => {
			unsubscribe();
		};
	}, []);
	return (
		<div style={{ height: 10000 }}>
			<div style={{ position: "fixed", top: 0, left: 0 }}>
				<p>Debounced top position: {position}</p>
				<p>Component rerendered {updatedCount.current} times</p>
			</div>
		</div>
	);
};
```

そこで用いるのが`useDebouncedCallback`です。第1引数に渡すコールバック関数で、状態変数(`position`)を設定します。そして、戻り値の関数`debounced`は、`subscribe`の第3引数に与えてください。

```react:src/ScrolledComponent.tsx
import { useDebouncedCallback } from 'use-debounce';

export const ScrolledComponent: FC = () => {

	const debounced = useDebouncedCallback(
		() => {
			setPosition(window.pageYOffset);
		},
		800,
	);
	useEffect(() => {
		/* const unsubscribe = subscribe(window, 'scroll', () => {
			setPosition(window.pageYOffset);
		}); */
		const unsubscribe = subscribe(window, 'scroll', debounced);
		return () => {
			unsubscribe();
		};
	}, []);

};
```

`useDebouncedCallback`でスクロール時の画面再描画を抑えたのがつぎのサンプル003です。スクロールする手を止めないかぎり、コンポーネント(`ScrolledComponent`)の再描画のカウンターは数値が上がりません。

#### サンプル003■React + TypeScript: useDebouncedCallback example 02 with use-debounce
https://codesandbox.io/embed/react-typescript-usedebouncedcallback-example-02-with-use-debounce-sp45wy?fontsize=14&hidenavigation=1&theme=dark