コールバックの呼び出しを間引く
コールバックが続けざまに呼び出されるとき、ひとつひとつを実行せず間引きたい場合があります。よくあるのは、テキストフィールドへの入力です(あるいは、スクロール時の処理など)。ひと文字ごとに実行するのでなく、ひと息ついたとき直近の値で処理できれば、負荷が下げられます。
そのようなときに用いるのが、debounceと呼ばれるテクニックです。立て続けに呼ばれるコールバックをすぐには実行せず、呼び出しが止んで一定時間が経ったら、最後のコールバックを実行します。そのために、react-useに備わるフックがuseDebounce
です。
useDebounce
の公式コード例
GitHubのuseDebounce
のページには、「Usage」としてコード例が示されています。CodeSandboxに「React + TypeScript: useDebounce of react-use 01」としてこの作例を掲げました。useDebounce
の構文はつぎのとおりです。
const [状態確認関数, キャンセル関数] = useDebounce(コールバック, 間隔ミリ秒, 依存配列)
GitHubの公式コード例(「Usage」)からフックの定めを抜き出しましょう。useDebounce
の第1引数は呼び出すコールバック関数です。第2引数には、最後の呼び出しから実行するまでに待つ時間をミリ秒で与えます。第3引数は依存配列で、要素(val
)の値が待ち時間経っても変わらなかったとき、直近のコールバックは実行されるのです。コード例ではこの値はテキストフィールドの入力値で、コールバック関数はその値を状態変数(debouncedValue
)に定めています。
const [state, setState] = React.useState('Typing stopped');
const [val, setVal] = React.useState('');
const [debouncedValue, setDebouncedValue] = React.useState('');
function App() {
const [, cancel] = useDebounce(
() => {
setState('Typing stopped');
setDebouncedValue(val);
},
2000,
[val]
);
}
前掲CodeSandbox作例(「React + TypeScript: useDebounce of react-use 01)は、テキストフィールド(<input type="text">
)に加えて状態変数値(debouncedValue
)もページに示しています。テキスト入力が一段落したら値が変わることを確かめられるでしょう。また、[Cancel debounce]ボタンをクリックすると時間待ちがキャンセルになり、入力を止めても状態変数値は変わりません。編集を再開すると、改めてdebounceは有効になります。
useDebounce
の構文
前掲公式コード例では、useDebounce
が返す配列の第1要素(インデックス0)の関数が使われていません。フックの構文を少し詳しく示しましょう。戻り値配列の第1要素(isReady
)はフックの実行状態を返し、第2要素(cancel
)が前掲作例の[Cancel debounce]ボタンで呼び出されたキャンセルの関数です。
const [
isReady: () => boolean | null,
cancel: () => void,
] = useDebounce(fn: Function, ms: number, deps: DependencyList = []);
-
fn
:Function
- 連続して呼ばれたとき、実行を間引くコールバック関数。 -
ms
:number
- 連続する呼び出しが一旦止まってから、最後のコールバックをあとから実行するまでの待ち時間(ミリ秒)。 -
deps
:DependencyList
- 依存配列。その要素の変化によりコールバックが呼び出される。 -
isReady
:() => boolean|null
- フックの現行の実行状態を3つの値で返す関数。-
false
- コールバックの呼び出しが続いていて、実行を待っている。 -
true
- コールバックの呼び出しが一旦止まって、待ち時間を過ぎ、最後の呼び出しが実行された。 -
null
- 最後のコールバックの呼び出し待ちがキャンセルされた。
-
-
cancel
:() => void
- 連続して呼ばれる最後のコールバックの実行をキャンセルする。そのあと改めて呼び出しはじめると、実行待ちが再開される。
戻り値配列第1要素の関数を使う
実行状態を返す関数(isReady
)の戻り値は3つです。ところが、この値をページに加えてみると、キャンセルしたときのnull
がなかなか示されません、[Cancel debounce]ボタンをクリックしても、ほとんどの場合false
のまま表示が変わらないのです。CodeSandboxに「React + TypeScript: useDebounce of react-use 02」としてサンプルを掲げました。
function App() {
// const [, cancel] = useDebounce(
const [isReady, cancel] = useDebounce(
);
return (
<div>
<div>
<button onClick={cancel}>Cancel debounce</button>
</div>
<div>{String(isReady())}</div>
</div>
);
}
問題は、キャンセルの関数(cancel
)を呼び出したとき、ページの再描画が起こらないことによるようです。これを避けるためには、関数を直にハンドラ(onClick
)に与えず、useCallback
フックに定めて、実行状態を返す関数(isReady
)は依存配列に加えます。
function App() {
const cancelDebounce = React.useCallback(() => {
cancel();
setState('Debounce canceled');
}, [cancel, isReady]);
return (
<div>
<div>
<button onClick={cancelDebounce}>Cancel debounce</button>
</div>
</div>
);
}
これで、[Cancel debounce]ボタンをクリックしたときuseCallback
フックで定めた関数(cancelDebounce()
)が呼び出され、依存配列に加えた実行状態の関数(isReady
)の戻り値が変わり、ページは再描画されます。
書き替えたルートモジュール(src/App.tsx
)の記述は、以下のコード001のとおりです。また、CodeSandboxにReact + TypeScript: useDebounce of react-use 03を作例として掲げました。なお、<input type="text">
要素のonChange
ハンドラの関数(typingText()
)もuseCallback
に切り分けています。
コード001■キャンセル関数をuseCallback
フックに定めた
import React from 'react';
import { useDebounce } from 'react-use';
function App() {
const [state, setState] = React.useState('Typing stopped');
const [val, setVal] = React.useState('');
const [debouncedValue, setDebouncedValue] = React.useState('');
const [isReady, cancel] = useDebounce(
() => {
setState('Typing stopped');
setDebouncedValue(val);
},
2000,
[val]
);
const typingText = React.useCallback(({ currentTarget }) => {
setState('Waiting for typing to stop...');
setVal(currentTarget.value);
}, [])
const cancelDebounce = React.useCallback(() => {
cancel();
setState('Debounce canceled');
}, [cancel, isReady]);
return (
<div>
<input
type="text"
value={val}
placeholder="Debounced input"
onChange={typingText}
/>
<div>{state}</div>
<div>
Debounced value: {debouncedValue}
</div>
<div>
<button onClick={cancelDebounce}>Cancel debounce</button>
</div>
<div>{String(isReady())}</div>
</div>
);
}
export default App;