同じコードが短い時間に繰り返し実行されるとき、処理を間引きたい場合があります。そのような制御(debounce)を行うライブラリがuse-debounce
です。
- 間引き処理に特化しているため、容量は1KBにも及びません。
- 使い方が標準的で、実装はUnderscoreやLodashと同じです。
- サーバーレンダリングにも対応しています。
間引きの処理が必要な例として本稿で採り上げるお題は、公式サイトを参考にしたつぎのふたつです。
- テキストフィールドに入力される値の変更を間引く。
- 値をAPIにリクエストするような場合に送信の負荷を下げる。
- 画面がスクロールされるときに再描画を減らす。
- スクロールする範囲が大きい場合に逐次描画し直さない。
インストール
インストールは、npmまたはyarnでつぎのように行ってください。
npm install --save use-debounce
yarn add use-debounce
useDebounce
で値の変更を間引く
まずは、間引きをしないテキストフィールドへの入力のコード例です。<input>
要素にひと文字入力するたびに、onChange
ハンドラで状態変数(text
)が更新されます。値は画面に「Actual value」(<p>
要素)として示しました。
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
です。変更を監視する値(value
)とその間隔のミリ秒(delay
)を引数に渡します。戻り値は配列で、第1要素(state
)が間引き後の値です。
const [state] = useDebounce(value, delay);
引数
-
value
: 変更を監視する値1。 -
delay
: 監視間隔のミリ秒。
戻り値
-
[state]
: 間引き後の状態値を第1要素にもつ配列。
useDebounce
関数には、テキスト入力フィールドの状態変数(text
)と監視間隔(1000
)を渡します。戻り値から得られる間引き後の値(value
)は、画面に「Debounce value:」(<p>
要素)として加えました。テキストフィールドの値が、入力のたびに変わることは同じです。けれど、間引き後の値は、入力を指定間隔止めなければ変更されません。以下のサンプル001に掲げたのはCodeSandboxに公開した作例です。動きや各モジュールのコードについては、こちらをご参照ください。
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
useDebouncedCallback
で関数の呼び出しを間引く
単純な値の変更ではなく、コールバック関数の呼び出しを間引くのがuseDebouncedCallback
です。第1引数にコールバック(func
)、第2引数には間引く呼び出し間隔(wait
)を渡してください。戻り値(debounced
)が呼び出しの間引かれる新たな関数です。
const debounced = useDebouncedCallback(func, wait);
引数
-
func
: 呼び出しを間引くコールバック関数。 -
wait
: 呼び出しを間引く間隔のミリ秒。
戻り値
-
debounced
: 呼び出しが間引かれる新たなコールバック関数。
もとにするコードは、前項と同じ冒頭のテキスト入力フィールドの例にしましょう。呼び出しが間引かれるコールバック関数(debounced
)は、onChange
ハンドラに定めます。すると、ハンドラ関数が状態変数(text
)を書き替えますので、間引かれたあとの値は別の状態変数にとっておく必要はありません(前掲サンプル001の変数value
)。
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
useDebouncedCallback
でスクロール画面の再描画を抑える
本稿のふたつめのお題です。スクロール画面の再描画をuseDebouncedCallback
で抑えてみましょう。画面のscroll
イベントは、ライブラリSubscribe-eventで捉えることにします。少し古いものの、扱いが簡単で小さなライブラリです。npmまたはyarnでインストールしてください。
npm install --save subscribe-event
yarn add subscribe-event
イベントを購読するsubscribe
関数の構文はつぎのとおりです。戻り値(unsubscribe
)が購読終了の関数であることにご注意ください。
const unsubscribe = subscribe(element, event, eventCallback);
引数
-
element
: イベントが引き起こされる要素。 -
event
: 購読するイベント。 -
eventCallback
: イベントのコールバック関数。
戻り値
-
unsubscribe
: イベントの購読を破棄する関数。
先に、useDebouncedCallback
は使わない画面のスクロールです。window
のscroll
イベントに応じてコールバック関数を呼び出します。ここでは、useEffect
に定めました。関数は単にwindow.pageYOffset
の値を状態変数(position
)に与えているだけです。コンポーネントアンマウント時の購読破棄(unsubscribe
)は、useEffect
の戻り値であるクリーンアップ関数から呼び出します。
状態変数値(position
)は、確認できるよう画面のテキスト「Debounced top position」(<p>
要素)に示しました。もうひとつ数値を与えたのが、ref
オブジェクト(updatedCount
)です(初期値0
)。コンポーネント(ScrolledComponent
)が描画されるたびにカウントアップされます。この再描画回数(updatedCount.current
)も、テキスト「Component rerendered nnn times」(<p>
要素)として画面に加えました。スクロールを続けていると、回数はどんどん増えることが確かめられるでしょう。
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引数に与えてください。
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
-
値が変更されたかどうかを確かめるのは、デフォルトでは単純な等価比較(
===
)です。そのため、対象がオブジェクトのときは、useDebouncedCallback
でプロパティ値を取り出して調べなければなりません(useDebounce
の第3引数にオプションオブジェクトで比較関数を渡すことはできます)。 ↩