👻

【小ネタ】ReactのカスタムフックでuseDebounceを実装する

2021/12/07に公開

アプリケーションなどを作っているとinputonChangeイベントで呼ばれる処理を間引いて何かを行いたい、みたいなことがあると思います。
例えばユーザーの入力内容をもとに検索のクエリを投げるとか、Reduxなどのstoreに反映させる必要があるなどが考えられます。
webサイトの制作でも例えばscrollresizeなど、大量に起こるイベントを間引いて処理を行いたい、みたいなことはたまにあります。

そういう場合、有名なものだとLodashdebouceですね。
https://lodash.com/docs/4.17.15#debounce

使い方は簡単です。

const debounced = debounce((e) => {
  console.log(`debouced: ${e.target.value}`)
}, 200)

document.querySelector('input').addEventListener('change', debounced);

これで200ms秒以上空いたタイミングでconsole.logが発火するということになります。
ただ、LodashdebouceをReactで使うのはちょっと厄介です。

LodashのdebounceをReactで使う

Lodashdebouceは、原理的にはsetTimeoutを呼んで、それが発火される前に再度呼び出しがあった場合は既存のtimerをクリアし、新たにsetTimeoutを呼ぶということを行っています。
つまりすでにsetTimeoutが存在するかを確認する必要があるため、useCallbackで囲ってやらないと常に新規のsetTimeoutが発火し、うまく動作しません。
しかしながら、useCallbackで囲うとuseCallbackは渡される関数がインラインでないと依存配列に何が必要かわからないため、Lintでwarningになってしまいます。

以下に参考実装を用意しました。

期待通りに動作しているのでLintのwarningを黙らせてしまうのもありですが、それだと依存配列の効かなくなってしまいます。

カスタムフックでuseDebounceを作る

そういうわけでそこまで面倒な処理でもないので自作していまいましょう。
そこでuseDebounceをで検索をかけると以下のような記事が見つかります。

https://usehooks.com/useDebounce/

zennで日本語だと以下の記事で上述の説明がされています。
https://zenn.dev/luvmini511/articles/4924cc4cf19bc9

もちろん、これでもいいのですが、形式がLodashのものとは違い、valueをもらってvalueを返すような形になっています。
それにuseEffectを利用して値の変更時の処理を書いているのも少々大変そうです。できればコールバック関数を渡して、自由度の高い処理がかけるようにしたいです。

というわけで書きました。

import { useCallback, useRef } from 'react';

type Debounce = (fn: () => void) => void;

export const useDebounce = (timeout: number): Debounce => {
  const timer = useRef<ReturnType<typeof setTimeout> | null>(null);
  const debounce: Debounce = useCallback(
    (fn) => {
      if (timer.current) {
        clearTimeout(timer.current);
      }
      timer.current = setTimeout(() => {
        fn();
      }, timeout);
    },
    [timeout]
  );
  return debounce;
};

非常に単純です。
useRefでミュータブルなrefオブジェクトを作成しています。ここにtimerを格納することで、コンポーネントが存在する間は同じものを参照することが可能になります。
使い方も単純です。予めtimeoutをもらっておいて、あとは使いたい関数をラップするだけです。

export default function App() {
  const [value, setValue] = useState("");
  const debounce = useDebounce(1000);

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setValue(e.target.value);
    debounce(() => {
      console.log(`debounced: ${e.target.value}`);
    });
  };

  return (
    <div className="App">
      <h1>Hello CodeSandbox</h1>
      <h2>Start editing to see some magic happen!</h2>
      <input value={value} onChange={handleChange} />
    </div>
  );
}

参考実装は以下です。

というわけで

useRefは主に実際のHTMLElementを格納するのに使用することが多いかと思いますが、こんな風にミュータブルな値を入れておくことも可能です。
案外知っておくと使う場面があるかもしれません。

そういうわけで良いReactライフを💃

Discussion