永遠にWIP

useStopTypingというHookを作った

React

はじめに

皆さんGoogle Slideなどのアプリケーションを使ったことはあるでしょうか?
Google Slideの機能の一つに一定時間操作がなかったら自動的に保存するというものがあります。
私のブログでは管理画面側で記事を作成して、公開非公開などを設定することが出来ます。
ここで、この一定時間操作がなかったら自動的に保存するという機能が管理画面の記事を書くところにはあったほうがいいだろうと思い、それを簡単に扱えるHookを作成しました!

useStopTyping

useStopTypingは、一定時間操作がない、またはフォーカスが外れたときにcallback関数を実行するHookになります。
useStopTypingという名前からその機能が出てくるのは分かりにくい。
どなたか、いい名前があったら教えてください...

https://www.npmjs.com/package/use-stop-typing
実際に例を見てみましょう。

import { useRef } from 'react';
import { useStopTyping } from 'use-stop-typing';

export default () => {
  const ref = useRef<HTMLInputElement>(null);
  useStopTyping(ref, () => console.log('Update'), 2000);

  return (
    <input type="text" ref={ref} />
  )
}

この例では、input要素に入力を始め、入力が止まったところから2000ms経ったところで`console.log('Update')が発火します。また、input要素に入力をし、途中でフォーカスを外したとき(つまりblur eventが発火したとき)も同様のことが起きます。

どういう仕組みになっているか

実際に中のコードを見てみます。

https://github.com/DuGlaser/use-stop-typing

実装自体はかなり小さく、40行ほどです。

import { RefObject, useEffect, useState } from 'react';

export const useStopTyping = (
  ref: RefObject<HTMLInputElement | HTMLTextAreaElement | null>,
  callback: () => void,
  setTimeoutMs: number
) => {
  const [refValue, setRefValue] = useState('');
  let typingTimer: ReturnType<typeof setTimeout>;

  const handleUpdate = () => {
    if (ref.current && refValue !== ref.current.value) {
      callback();
      clearTimeout(typingTimer);
      setRefValue(ref.current.value);
    }
  };

  const handleKeyUp = () => {
    clearTimeout(typingTimer);
    typingTimer = setTimeout(handleUpdate, setTimeoutMs);
  };

  const handleKeyDown = () => {
    clearTimeout(typingTimer);
  };

  useEffect(() => {
    if (ref.current) {
      ref.current.addEventListener('keyup', handleKeyUp);
      ref.current.addEventListener('keydown', handleKeyDown);
      ref.current.addEventListener('blur', handleUpdate);
    }

    return () => {
      if (ref.current) {
        ref.current.removeEventListener('keyup', handleKeyUp);
        ref.current.removeEventListener('keydown', handleKeyDown);
        ref.current.removeEventListener('blur', handleUpdate);
      }
    };
  }, [ref, refValue, handleUpdate]);
};

やっていることはかなり単純で、keyupしたときにsetTimeoutを使ってcallback関数を一定時間後に実行するようにしています。また、KeydonwしたときはclearTimeoutを実行して、callback関数が実行されないようにしています。

また、内部にStateを持っている理由としては入力が始まる前と止まってcallbackが呼び出されるときにinputの内容が変化しているか判断するためです。
これがないと矢印キーなどを押しただけでもcallback関数が実行される可能性があります。

最後に

ちなみに一番苦労したところはreact-testing-libraryがしっかり動く環境を作るところでした...

© 2020 DuGlaser