🤗

まだuseEffectとsetIntervalを使ってタイマー処理してるの?

に公開

もっと簡単な方法があるよ!

Reactでポーリング処理やタイマー処理を実装するとき、こんなコードを書いていませんか?

const timerRef = useRef<number | null>(null);

useEffect(() => {
  executeQuery();

  timerRef.current = window.setInterval(() => {
    executeQuery();
  }, 3000);

  return () => {
    if (timerRef.current !== null) {
      clearInterval(timerRef.current);
    }
  };
}, [executeQuery]);

毎回こんなボイラープレートコードを書くのは面倒ですよね...😓

  • タイマーIDを保持するためのuseRefを用意する
  • クリーンアップ処理を忘れずに書く
  • 依存配列を適切に管理する
  • そもそもコードが長くて読みにくい

実は便利なカスタムフックがあるんです!

react-useライブラリが提供するuseIntervalを使えば、たった1行でこの処理が書けるんです!

import useInterval from "react-use/lib/useInterval";

useInterval(executeQuery, 1000);

え?たったこれだけ?そう、これだけなんです!😲

どうしてこんなに簡単になるの?

useIntervalは内部で以下のことを全部やってくれます:

  1. ✅ タイマーIDの管理
  2. ✅ コンポーネントのアンマウント時の自動クリーンアップ
  3. ✅ 依存配列の適切な処理
  4. ✅ コードの簡潔化

実際どう動いているの?

気になる方のために、useIntervalの簡略化した実装を見てみましょう:

import { useEffect, useRef } from 'react';

function useInterval(callback: () => void, delay: number | null) {
  const savedCallback = useRef<() => void>();

  // コールバック関数を保存
  useEffect(() => {
    savedCallback.current = callback;
  }, [callback]);

  // インターバルの設定
  useEffect(() => {
    function tick() {
      savedCallback.current?.();
    }
    
    if (delay !== null) {
      const id = setInterval(tick, delay);
      return () => clearInterval(id);
    }
  }, [delay]);
}

なるほど!内部では結局useEffectsetIntervalを使っているんですね。でも、この複雑さをカスタムフックが隠蔽してくれるので、私たちは簡潔なコードを書けるわけです。

他にも便利なタイマー系フックがあるよ!

react-useライブラリには他にもこんな便利なフックがあります:

  • useTimeout - 「3秒後に何かしたい」ときに便利
  • useDebounce - 「入力が終わってから検索したい」ときに便利
  • useThrottle - 「スクロールイベントの処理回数を制限したい」ときに便利

useTimeoutの具体例

「成功メッセージを3秒後に自動的に消したい」というケースを考えてみましょう。

従来の書き方

const SuccessMessage: FC = () => {
  const [visible, setVisible] = useState(true);
  const timeoutRef = useRef<number | null>(null);

  useEffect(() => {
    timeoutRef.current = window.setTimeout(() => {
      setVisible(false);
    }, 3000);

    return () => {
      if (timeoutRef.current !== null) {
        clearTimeout(timeoutRef.current);
      }
    };
  }, []);

  if (!visible) return null;
  
  return <div className="success-message">操作が成功しました!</div>;
};

useTimeoutを使った書き方

import useTimeout from "react-use/lib/useTimeout";

const SuccessMessage: FC = () => {
  const [visible, setVisible] = useState(true);
  
  useTimeout(() => {
    setVisible(false);
  }, 3000);

  if (!visible) return null;
  
  return <div className="success-message">操作が成功しました!</div>;
};

たったこれだけで、タイマーIDの管理やクリーンアップの心配をする必要がなくなりました!

useDebounceの具体例

検索ボックスで「ユーザーが入力を止めてから検索APIを呼び出したい」というケースはよくありますよね。

従来の書き方

const SearchBox: FC = () => {
  const [query, setQuery] = useState("");
  const [debouncedQuery, setDebouncedQuery] = useState("");
  const timeoutRef = useRef<number | null>(null);

  useEffect(() => {
    if (timeoutRef.current !== null) {
      clearTimeout(timeoutRef.current);
    }

    timeoutRef.current = window.setTimeout(() => {
      setDebouncedQuery(query);
    }, 500);

    return () => {
      if (timeoutRef.current !== null) {
        clearTimeout(timeoutRef.current);
      }
    };
  }, [query]);

  useEffect(() => {
    if (debouncedQuery) {
      searchAPI(debouncedQuery);
    }
  }, [debouncedQuery]);

  return (
    <input
      type="text"
      value={query}
      onChange={(e) => setQuery(e.target.value)}
      placeholder="検索..."
    />
  );
};

useDebounceを使った書き方

import { useDebounce } from "react-use";

const SearchBox: FC = () => {
  const [query, setQuery] = useState("");
  const [debouncedQuery] = useDebounce(query, 500);

  useEffect(() => {
    if (debouncedQuery) {
      searchAPI(debouncedQuery);
    }
  }, [debouncedQuery]);

  return (
    <input
      type="text"
      value={query}
      onChange={(e) => setQuery(e.target.value)}
      placeholder="検索..."
    />
  );
};

コードがこんなにスッキリしました!useDebounceが内部でタイマー処理を全部やってくれるので、私たちはロジックに集中できます。

useThrottleの具体例

「スクロールイベントの処理回数を制限したい」というケースを考えてみましょう。

従来の書き方

const ScrollTracker: FC = () => {
  const [scrollY, setScrollY] = useState(0);
  const throttlingRef = useRef(false);
  const timeoutRef = useRef<number | null>(null);

  useEffect(() => {
    const handleScroll = () => {
      if (throttlingRef.current) return;
      
      throttlingRef.current = true;
      setScrollY(window.scrollY);
      
      timeoutRef.current = window.setTimeout(() => {
        throttlingRef.current = false;
      }, 200);
    };

    window.addEventListener("scroll", handleScroll);
    
    return () => {
      window.removeEventListener("scroll", handleScroll);
      if (timeoutRef.current !== null) {
        clearTimeout(timeoutRef.current);
      }
    };
  }, []);

  return <div>現在のスクロール位置: {scrollY}px</div>;
};

useThrottleを使った書き方

import { useThrottle } from "react-use";

const ScrollTracker: FC = () => {
  const [scrollY, setScrollY] = useState(0);
  const throttledScrollY = useThrottle(scrollY, 200);

  useEffect(() => {
    const handleScroll = () => {
      setScrollY(window.scrollY);
    };

    window.addEventListener("scroll", handleScroll);
    return () => window.removeEventListener("scroll", handleScroll);
  }, []);

  return <div>現在のスクロール位置: {throttledScrollY}px</div>;
};

useThrottleを使うことで、スロットリングのロジックを自分で実装する必要がなくなりました!

useMountの具体例

コンポーネントがマウントされた時に一度だけ処理を実行したいケースはよくありますよね。例えば、「ページが表示された時に初期データを取得したい」というシチュエーションを考えてみましょう。

従来の書き方

const UserProfile: FC = () => {
  const [userData, setUserData] = useState(null);

  useEffect(() => {
    // マウント時に一度だけ実行される
    fetchUserData().then(data => {
      setUserData(data);
    });
    // 空の依存配列を指定して、マウント時のみ実行されるようにする
  }, []);

  return (
    <div className="user-profile">
      {userData ? (
        <div>ユーザー名: {userData.name}</div>
      ) : (
        <div>読み込み中...</div>
      )}
    </div>
  );
};

この書き方には問題があります:

  1. 空の依存配列[]を忘れると無限ループになる危険性がある
  2. ESLintのルールによっては警告が出ることがある
  3. コードの意図(マウント時のみ実行)が明示的でない

useMountを使った書き方

import { useMount } from "react-use";

const UserProfile: FC = () => {
  const [userData, setUserData] = useState(null);

  useMount(() => {
    // マウント時に一度だけ実行される
    fetchUserData().then(data => {
      setUserData(data);
    });
  });

  return (
    <div className="user-profile">
      {userData ? (
        <div>ユーザー名: {userData.name}</div>
      ) : (
        <div>読み込み中...</div>
      )}
    </div>
  );
};

useMountを使うことで:

  1. ✅ コードの意図が明確になる(「マウント時に実行」という目的がフック名で表現されている)
  2. ✅ 依存配列を間違える心配がない
  3. ✅ ESLintの警告が出にくい
  4. ✅ コードがより簡潔になる

useMountの実装

内部的には非常にシンプルです:

import { useEffect } from 'react';

const useMount = (fn: () => void) => {
  useEffect(() => {
    fn();
  }, []); // 空の依存配列で、マウント時のみ実行
};

たったこれだけです!でも、このシンプルなラッパーが、コードの意図を明確にし、バグを減らすのに役立ちます。

まとめ

カスタムフックを活用すれば、コードはシンプルに、バグは少なく、開発効率は上がります!特にuseIntervaluseMountのような便利なフックは、日々の開発で頻繁に使う処理をグッと簡単にしてくれます。

次回コードレビューで「このuseEffectと空の依存配列、useMountで書き直せるよ〜」って言えば、きっとチームメイトに感謝されますよ!😉

Discussion