例えば現在の時刻をリアルタイムに表示させたい場合、カスタムフック useWatch
を作って、そこから更新され続ける time
を受け取りたい。
const App: React.FC = () => {
const time = useWatch(1000);
return <span>{dayjs(time).format('HH:mm')}</span>
};
結論
use-watch.ts
import { useEffect, useState } from 'react';
export default function useWatch(interval: number) {
const [time, updateTime] = useState(Date.now());
useEffect(() => {
const timeoutId: number = setTimeout(() => updateTime(Date.now()), interval);
return () => {
clearTimeout(timeoutId);
};
}, [time]); // eslint-disable-line react-hooks/exhaustive-deps
return time;
}
-
useEffect
のコールバックが実行開始。 -
interval
ミリ秒後にupdateTime
されてtime
が更新される。 -
time
が更新されたのでuseEffect
が反応。 - クリーンアップがあるため、クリーンアップを実行。
clearTimeout
される。 - 1〜4 を繰り返す。
- 利用しているコンポーネントがアンマウントされたときに、クリーンアップを実行。
clearTimeout
される。
結論は、 useEffect
のコールバックが繰り返し実行されるので再帰呼び出しは不要。
間違った実装
import { useEffect, useState } from 'react';
export default function useWatch(interval: number) {
const [time, updateTime] = useState(Date.now());
const update = () => {
updateTime(Date.now());
const id = setTimeout(update, interval);
return () => {
clearTimeout(id);
};
};
useEffect(update, []);
return time;
}
update
関数を作って setTimeout
で再帰呼び出しを行っているので、 time
は問題なく一見うまくいっているように見える。
しかし、 コンポーネントのアンマウントによってクリーンアップがされた場合、クリーンアップ関数のスコープの id
は初回の値で固定されているため、ただしく clearTimeout
ができなくなる。
クリーンアップ関数のスコープの id
を随時更新するには useEffect
の依存変数を調整することになるが、その時点で useEffect
のコールバックが繰り返し呼び出されることになるので、先の結論の実装に落ち着く。