【小ネタ】ReactのカスタムフックでuseDebounceを実装する
アプリケーションなどを作っているとinput
のonChange
イベントで呼ばれる処理を間引いて何かを行いたい、みたいなことがあると思います。
例えばユーザーの入力内容をもとに検索のクエリを投げるとか、Redux
などのstoreに反映させる必要があるなどが考えられます。
webサイトの制作でも例えばscroll
やresize
など、大量に起こるイベントを間引いて処理を行いたい、みたいなことはたまにあります。
そういう場合、有名なものだとLodash
のdebouce
ですね。
使い方は簡単です。
const debounced = debounce((e) => {
console.log(`debouced: ${e.target.value}`)
}, 200)
document.querySelector('input').addEventListener('change', debounced);
これで200ms秒以上空いたタイミングでconsole.log
が発火するということになります。
ただ、Lodash
のdebouce
をReactで使うのはちょっと厄介です。
LodashのdebounceをReactで使う
Lodash
のdebouce
は、原理的にはsetTimeout
を呼んで、それが発火される前に再度呼び出しがあった場合は既存のtimer
をクリアし、新たにsetTimeout
を呼ぶということを行っています。
つまりすでにsetTimeout
が存在するかを確認する必要があるため、useCallback
で囲ってやらないと常に新規のsetTimeout
が発火し、うまく動作しません。
しかしながら、useCallback
で囲うとuseCallback
は渡される関数がインラインでないと依存配列に何が必要かわからないため、Lintでwarningになってしまいます。
以下に参考実装を用意しました。
期待通りに動作しているのでLintのwarningを黙らせてしまうのもありですが、それだと依存配列の効かなくなってしまいます。
カスタムフックでuseDebounceを作る
そういうわけでそこまで面倒な処理でもないので自作していまいましょう。
そこでuseDebounce
をで検索をかけると以下のような記事が見つかります。
zennで日本語だと以下の記事で上述の説明がされています。
もちろん、これでもいいのですが、形式が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