React カスタムフックでDebounceしてみる
まえがき
はい。こんにちは。
業務で「このボタン連打防止の機能つけてほしいな~...でもdisableにはしないでほしいな~....」と言われたので、ref
への理解を深めるためにも練習がてらuseDebounce
というカスタムhooksを作りました。
いろんな記事を参考にさせていただいたのですが、関数そのものをdebounceさせるような記事があまりなく、残しとくか!という感じで記事書いてます。色々考慮できてない点があるような気がしてますが、今のところ思った通りに動いているし、まあOK...!!!
とりあえず完成形
import { useCallback, useRef } from 'react';
type Props = {
callback: () => void;
delay?: number;
};
export const useDebounce = ({
callback,
delay = 5_000,
}: Props) => {
const timerRef = useRef<NodeJS.Timer | null>(null);
const debounce = useCallback(() => {
if (timerRef.current) clearTimeout(timerRef.current);
timerRef.current = setTimeout(() => {
callback();
}, delay);
}, [delay, callback]);
return debounce;
};
使用側
import { React } from 'react'
import { useDebounce } from '../hooks/useDebounce'
export const Page: React.FC = () => {
const debounceThankyou = useDebounce({
callback: () => console.log("読んでくれてサンキューな!! また読んでくれよな!!")
delay: 300_000
})
return (
<div>
...
<button onClick={debounceThankyou}>デバウンス!!</button>
</div>
)
}
debounceとは
参考記事をみていただいた方が詳しく知れると思うので、特に深く言及しませんが、
適当に説明すると、「連打しても無駄...。1回しかcallしてあげないもんね...!」 っていうやつです。僕が実装した上記のhooksでは最後の1回だけcallするようになっています。
「ボタン10回押したら10回APIをcallされると困るなぁ」と言う時にお使いください。
処理の流れ
全体の処理を一言?で言うと
「delay秒以内に連打したら、前回の分は無視して、今からdelay秒後にcallbackするよ!」 です。
ではconst debounce = () => {...}
の中身を見ていきましょう。とは言っても単純なhooksですが...。
ここです
const timerRef = useRef<NodeJS.Timer | null>(null); // 1
const debounce = useCallback(() => {
if (timerRef.current) clearTimeout(timerRef.current); // 2
timerRef.current = setTimeout(() => { // 3
callback();
}, delay);
}, [func, callback]);
(1) const timerRef = useRef<NodeJS.Timer | null>(null);
まずはuseRef
でsetTimeout関数を入れておくRefを生成します。
localなstate(useState
)だと、レンダーされるたびに新しいstateが作られてしまうので2回目以降に前回の分のsetTimeout
を止めることができなくなってしまいます。
Reactではレンダーされる度に世界線が変わる。と考えるととても理解しやすいですね。(おかげで私も理解できました。ありがとう🖖)
(2) if (timerRef.current) clearTimeout(timerRef.current);
前回分のsetTimeout関数がRefの中にあれば、一旦clearTimeout
でリセットしておきます。
ボタンを連打していた場合は、前回の分のRefをリセットしてまっさらな世界にしてから新しいsetTimeout関数をスタートさせるわけです。
(3) timerRef.current = setTimeout(() => {
callback();
}, delay);
ただただsetTimeout
するだけだと、次回ボタンを押した時に「え?今初めてボタン押したんやっけ?2回目やっけ?」ってなるので、timerRef
の中に入れておきましょう。こうすることで (2)の処理が可能になるわけです。(ちなみにdelayにはPropsで渡ってきたMSが入ります。)
あとがき
と言うわけでdebounceさせるためのhooksを実装してみました。
正直Refが絡んでくるだけで「...ん?....んん??」ってなってしまうので、もうちょっと慣れていきたいですね。次は練習がてらusePolling
みたいなhooksでも作ってみようかと思います。
... 読んでくれてサンキューな!! また読んでくれよな!!
参考
参考にさせていただきました。ありがとうございます!
Discussion
もしかして、
いらないのでは...?