【React x TypeScript】useDebounce を作る


こんにちは Gaji-Labo フロントエンドエンジニアの茶木です。

inputscroll など連続して発生するイベントで遅延が発生するほど高負荷ならば、処理をまびく debounce の出番ですね。onScrollonChange の処理結果の多くは、後続の処理で上書きされるので、連続した処理の最後だけ行って、途中の処理はスキップしても問題ないものがほとんどだからです。

React では hook の形で準備すると便利です。

基本形

debounce の機能は、処理(A)を少し遅らせて実行する timeout の処理と、その実行前に次の処理(B)の要求を受けたときにAの timeout をクリアして、Bの timeout をセットすることです。

理解のために、素直に書くとこうです。

let timerId:NodeJS.Timeout;
const debounce = (callback:() => void) => {
  if(timerId) {
    clearTimeout(timer.current);
    timer.current = undefined;
  }
  timerId = setTimeout(callback, 40);
}
  • timeout が動いていれば( = 実行待ちの古い処理があれば)破棄する
  • 処理を timeout で遅延実行する

やってることは簡単ですね。

debounce をカスタムフックにする

基本形では、debounce の引数に呼び出す関数を入れていました。そのため例えば onChange で使う場合は毎回 debounce でくるむ形になってしまいそうです。

onChange={debounce(() => handleInput(e))}

debounce を気にせずロジックに集中して書けるとなお良いですよね。
上記を加味したカスタムフックの debounce を作ってみました。

const useDebounce = <T>(
  callback: (arg: T) => void,
  delay: number
): [(arg: T) => void, () => void] => {
  const timer = useRef<NodeJS.Timeout>();
  const clearDebounce = useCallback(() => {
    if (timer.current) {
      clearTimeout(timer.current);
      timer.current = undefined;
    }
  }, []);
  const debounce = useCallback(
    (arg: T) => {
      clearDebounce();
      timer.current = setTimeout(() => {
        callback(arg);
      }, delay);
    },
    [callback, clearDebounce, delay]
  );
  return [debounce, clearDebounce];
};

変更点

いくつか基本形からの変更があるので解説します。

callback は事前登録

  • callbackonChange の時点で書かずに済むように、useDebounce の引数に入れています。
  • あわせて callback の引数(多くは e, イベントオブジェクトになるでしょう)を受け取れるようにしています。

debounde の破棄用のメソッド clearDebounce

  • useDebounce が返す配列に、手動で現在登録中のdebounce を破棄するメソッド clearDebounce を含めました。

useDebounceを呼び出して使う

オリジナルのイベントハンドラ想定のメソッドを、 useDebounceに処理遅延の時間と一緒に食わせると、debounce機能付きのイベントハンドラになります。

これで onChangeは、中で debounce が行われてることなど気にせず記述ができます。

const changeInput = (e) => { /* do something */ }
const [handleInput, clearDebounceHandleInput] = useDebounce(changeInput, 40);

return (
  <input onChange={handleInput}>
);

また clearDebounce を使って、たとえば、コンポーネントが破棄されるタイミングで未処理のdebounceが残っていると不都合がある場合に cleanup に指定して処理を破棄できます。

useEffect(() => {
  return clearDebounceHandleInput;
})

これで安心して重たい処理が input や scroll で使えますね!
(そもそもイベントハンドラは重たくならないように気をつけないといけないけど・・・)

Gaji-Laboでは、React経験が豊富なフロントエンドエンジニアを募集しています

弊社ではReactの知見で事業作りに貢献したいフロントエンドエンジニアを募集しています。大きな制作会社や事業会社とはひと味もふた味も違うGaji-Laboを味わいに来ませんか?

もちろん、一緒にお仕事をしてくださるパートナーさんも随時募集中です。まずはお気軽に声をかけてください。お仕事お問い合わせや採用への応募、共に大歓迎です!

求人応募してみる!

投稿者 Chaki Hironori

webライターもやってるフロントエンドエンジニアです。Reactは自信があります。またデザイン畑の出身で、気持ちのいいアニメーションやインタラクティブな表現は丁寧に手掛けます。好きなものは中南米の遺跡で、スペイン語が少しできます。