🌝

React カスタムフックでDebounceしてみる

2022/05/21に公開
1

まえがき

はい。こんにちは。
業務で「このボタン連打防止の機能つけてほしいな~...でもdisableにはしないでほしいな~....」と言われたので、refへの理解を深めるためにも練習がてらuseDebounceというカスタムhooksを作りました。

いろんな記事を参考にさせていただいたのですが、関数そのものをdebounceさせるような記事があまりなく、残しとくか!という感じで記事書いてます。色々考慮できてない点があるような気がしてますが、今のところ思った通りに動いているし、まあOK...!!!

とりあえず完成形

src/hooks/useDebounce.ts
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;
};
使用側
src/page.tsx
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でも作ってみようかと思います。

... 読んでくれてサンキューな!! また読んでくれよな!!

参考

参考にさせていただきました。ありがとうございます!

https://zenn.dev/luvmini511/articles/4924cc4cf19bc9
https://rios-studio.com/tech/react-hookにおけるtimeoutとtimeinterval【止まらない・重複する】

Discussion