React のカスタムフックで作る debounce 機能

2021/06/09に公開

始め

最近案件で debounce 機能を実装する機会がありました。色々調べてたら、カスタムフックで実装できる方法があったので紹介したいと思います。

まだカスタムフックに馴染んでない方は「React 初心者の難問、カスタムフック(Custom Hook)を解説します」を是非お読みください。

1. debounceとは

私が実装してたのは検索欄に文字を打つたびにサジェストを表示する機能でした。普通は検索するとき単語をぱぱっと連続で入力しますので、インプットが発生するたびに API コールするよりは入力が終わった後に API コールしたほうが効率的です。ここで欲しくなるのが debounce です。

debounceとは連続で実装された関数たちの中で一番最後の関数だけ(もしくは一番最初の関数だけ)実装させることです。

簡単なデモを準備しました。インプット欄になにか入力してみてください。

原理は入力が発生するたびにタイマーを設定して一定時間、上記のデモだったら500msの間入力が発生しなかったら入力が終わったとみなして関数を実行することです。500msが経つ前に入力が発生したら前のタイマーはキャンセルして新しいタイマーを設定します。

ですでの、setTimeoutclearTimeoutを使ったら debounce が実装できます。このデモは Vanilla JS で実装してますが、今回は React の カスタムフックを使って実装してみましょう。

2. カスタムフック作成

それではuseDebounceというカスタムフックを作成します。このフックは debounce の対象になる値とディレイ時間を引数として受け取ります。

import { useState, useEffect } from "react";

export function useDebounce(value, delay) {
 // debounce の対象 state と setter
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    // delay 後 debounce の対象 state をアップデート
    const timer = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    // 次の effect が実行される直前に timer キャンセル
    return () => {
      clearTimeout(timer);
    };
    
  // value、delay がアップデートするたびに effect 実行
  }, [value, delay]);

  // 最終的にアップデートされた state をリターン
  return debouncedValue;
}

useStateuseEffectだけで実装できるし、結構簡単ですね。なるほどー!と思った部分はclearTimeoutをクリーンアップ部分で処理してることです。

useEffectは関数をリターンすることができて、それをクリーンアップと言います。クリーンアップ関数が実行されるタイミングは第2引数の配列によって決まります。

  • useEffectの第2引数が空の配列の場合: コンポーネントがアンマウントされる直前
  • useEffectの第2引数配列に値がある場合: コンポーネントがアップデートされる直前

useDebounceの場合はvaluedelayに依存しているので、この値たちが更新されてコンポーネントがアップデートされる直前にクリーンアップ関数が実行されることになります。

それを踏まえてuseDebounceがやってることを整理してみるとこうなります。

  1. debouncedValueの初期値は引数で渡されたvalue
  2. valueが更新されてtimerが走る
  3. delay時間が経つ前にvalue更新
  4. clearTimeoutによって前のtimerキャンセル
  5. コンポーネントがアップデートされてまたtimerが走る
  6. delay時間が経つまでvalueの更新がない
  7. debouncedValueリターン

3. カスタムフック使用

さて、カスタムフックを作成したので使ってみましょう。カスタムフックを使ってるデモも用意しましたので、使ってみてください。

コードを見てみたらこんな感じです。APIコールする処理を入れたかったですが、丁度いいものが思いつかなくてconsole.logを代用しました。

  const [inputText, setInputText] = useState("");
  const debouncedInputText = useDebounce(inputText, 500);
  const handleChange = (event) => setInputText(event.target.value);

  // APIコールなどは useEffect の中で
  useEffect(() => {
    console.log(`${debouncedInputText}」 に対するAPIコール`);
  }, [debouncedInputText]);

  return (
    <div className="App">
      <p>入力してください</p>
      <input type="text" onChange={handleChange} />
      <p>{debouncedInputText}</p>
    </div>
  );

useDebounceが返す値を変数に入れといて、その変数に依存するuseEffectを作成します。APIコールの処理はそのuseEffectの中に入れておくと、入力が起きるたびにAPIが走ることなく決めた時間が経った後にAPIコールすることができるようになります。

私はこれで200ms間入力がない場合だけサジェストを表示する機能を実装しました。

終わり

実装してみたら Vanilla JS よりこちらのほうがスッキリしててわかりやすい気がしますね。

GitHubで編集を提案

Discussion