React のカスタムフックで作る debounce 機能
始め
最近案件で debounce 機能を実装する機会がありました。色々調べてたら、カスタムフックで実装できる方法があったので紹介したいと思います。
まだカスタムフックに馴染んでない方は「React 初心者の難問、カスタムフック(Custom Hook)を解説します」を是非お読みください。
1. debounceとは
私が実装してたのは検索欄に文字を打つたびにサジェストを表示する機能でした。普通は検索するとき単語をぱぱっと連続で入力しますので、インプットが発生するたびに API コールするよりは入力が終わった後に API コールしたほうが効率的です。ここで欲しくなるのが debounce です。
debounceとは連続で実装された関数たちの中で一番最後の関数だけ(もしくは一番最初の関数だけ)実装させることです。
簡単なデモを準備しました。インプット欄になにか入力してみてください。
原理は入力が発生するたびにタイマーを設定して一定時間、上記のデモだったら500msの間入力が発生しなかったら入力が終わったとみなして関数を実行することです。500msが経つ前に入力が発生したら前のタイマーはキャンセルして新しいタイマーを設定します。
ですでの、setTimeout
とclearTimeout
を使ったら debounce が実装できます。このデモは Vanilla JS で実装してますが、今回は React の カスタムフックを使って実装してみましょう。
2. カスタムフック作成
それではuseDebounce
というカスタムフックを作成します。このフックは debounce の対象になる値とディレイ時間を引数として受け取ります。
import { useState, useEffect } from "react";
export function useDebounce(value, delay) {
// debounce の対象 state と setter
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
// delay 後 debounce の対象 state をアップデート
const timer = setTimeout(() => {
setDebouncedValue(value);
}, delay);
// 次の effect が実行される直前に timer キャンセル
return () => {
clearTimeout(timer);
};
// value、delay がアップデートするたびに effect 実行
}, [value, delay]);
// 最終的にアップデートされた state をリターン
return debouncedValue;
}
useState
とuseEffect
だけで実装できるし、結構簡単ですね。なるほどー!と思った部分はclearTimeout
をクリーンアップ部分で処理してることです。
useEffect
は関数をリターンすることができて、それをクリーンアップと言います。クリーンアップ関数が実行されるタイミングは第2引数の配列によって決まります。
-
useEffect
の第2引数が空の配列の場合: コンポーネントがアンマウントされる直前 -
useEffect
の第2引数配列に値がある場合: コンポーネントがアップデートされる直前
useDebounce
の場合はvalue
とdelay
に依存しているので、この値たちが更新されてコンポーネントがアップデートされる直前にクリーンアップ関数が実行されることになります。
それを踏まえてuseDebounce
がやってることを整理してみるとこうなります。
-
debouncedValue
の初期値は引数で渡されたvalue
-
value
が更新されてtimer
が走る -
delay
時間が経つ前にvalue
更新 -
clearTimeout
によって前のtimer
キャンセル - コンポーネントがアップデートされてまた
timer
が走る -
delay
時間が経つまでvalue
の更新がない -
debouncedValue
リターン
3. カスタムフック使用
さて、カスタムフックを作成したので使ってみましょう。カスタムフックを使ってるデモも用意しましたので、使ってみてください。
コードを見てみたらこんな感じです。APIコールする処理を入れたかったですが、丁度いいものが思いつかなくてconsole.log
を代用しました。
const [inputText, setInputText] = useState("");
const debouncedInputText = useDebounce(inputText, 500);
const handleChange = (event) => setInputText(event.target.value);
// APIコールなどは useEffect の中で
useEffect(() => {
console.log(`「${debouncedInputText}」 に対するAPIコール`);
}, [debouncedInputText]);
return (
<div className="App">
<p>入力してください</p>
<input type="text" onChange={handleChange} />
<p>{debouncedInputText}</p>
</div>
);
useDebounce
が返す値を変数に入れといて、その変数に依存するuseEffect
を作成します。APIコールの処理はそのuseEffect
の中に入れておくと、入力が起きるたびにAPIが走ることなく決めた時間が経った後にAPIコールすることができるようになります。
私はこれで200ms間入力がない場合だけサジェストを表示する機能を実装しました。
終わり
実装してみたら Vanilla JS よりこちらのほうがスッキリしててわかりやすい気がしますね。
Discussion