【ITニュース解説】Coding Challenge Practice - Question 10
2025年09月19日に「Dev.to」が公開したITニュース「Coding Challenge Practice - Question 10」について初心者にもわかりやすく解説しています。
ITニュース概要
Reactで`setTimeout`を簡単に扱う`useTimeout`フックの作成法を紹介。`callback`変更ではタイマーを維持し、`delay`変更でリセットする複雑なタイマー制御を、`useRef`と`useEffect`で実装する手法を解説する。
ITニュース解説
このニュース記事は、ReactというJavaScriptのライブラリを使ってWebアプリケーションを開発する際に役立つ、「カスタムフック」の作成方法について解説している。具体的には、一定時間後に特定の処理を実行するsetTimeoutというJavaScriptの標準機能を、Reactのコンポーネント内でより安全かつ効率的に利用するためのuseTimeoutというカスタムフックをどのように実装するかを説明している。
まず、JavaScriptのsetTimeout関数は、指定したミリ秒数が経過した後に、引数で渡された関数(コールバック関数)を実行する。これは非同期処理の一つで、Webアプリケーションではアニメーションの遅延やデータ取得後の表示更新など、さまざまな場面で利用される基本的な機能だ。しかし、Reactの関数コンポーネントでは、コンポーネントが再レンダリングされるたびに関数全体が再実行されるため、単純にsetTimeoutを呼び出すだけでは意図しない動作を引き起こす可能性がある。例えば、コンポーネントが頻繁に更新されると、そのたびに新しいタイマーが設定されてしまい、古いタイマーが残り続けてメモリリークの原因になったり、複数の処理が同時に実行されたりする問題が発生する。
そこで登場するのが、Reactの提供するフックだ。フックを使うことで、関数コンポーネントに状態管理やライフサイクル管理などの機能を追加できる。このuseTimeoutフックの目的は二つある。一つは、callback(実行したい関数)とdelay(遅延時間)を渡すだけでsetTimeoutを簡単に利用できるようにすること。もう一つは、delayの値が変更された場合にはタイマーをリセットし、新しい遅延時間でタイマーを再設定するが、callbackの内容が変わっただけではタイマーをリセットしない、という特定の振る舞いを実現することだ。
この要件を実現するために、二つの主要なフックであるuseRefとuseEffectが使われている。
まず、callbackが変更されてもタイマーをリセットしない、という要件について説明する。Reactの関数コンポーネントでは、コンポーネントが再レンダリングされるたびに、その中で定義された関数も新しく生成される可能性がある。もしcallback関数を直接useEffectの依存配列に入れてしまうと、callbackが変更されるたびにuseEffect全体が再実行され、タイマーがリセットされてしまう。これを避けるためにuseRefが利用される。useRefは、コンポーネントが再レンダリングされてもその値が保持され続ける特別なオブジェクトを生成するフックだ。savedCallback.currentという形で、その保持された値にアクセスしたり、更新したりできる。
記事の最初のuseEffectは、このuseRefを使ってcallbackの最新バージョンをsavedCallback.currentに保存する役割を担っている。
1const savedCallback = useRef(callback); 2useEffect(() => { 3 savedCallback.current = callback; 4}, [callback]);
このコードでは、savedCallbackというuseRefオブジェクトを初期化し、そのcurrentプロパティに、useTimeoutフックに渡されたcallback関数を保存している。続くuseEffectは、依存配列に[callback]が指定されているため、callbackが変更されるたびに実行される。これにより、savedCallback.currentは常に最新のcallback関数を指すようになる。しかし、このuseEffect自体はタイマーの設定・解除を行わないため、callbackの変更によるタイマーのリセットは発生しない。タイマーは、常にsavedCallback.current()を呼び出すことで、最新の処理内容を実行できるようになる。
次に、delayの変更とタイマーの管理について説明する。これは二つ目のuseEffectで実現されている。
1useEffect(() => { 2 if (delay == null) return; 3 const id = setTimeout(() => { 4 savedCallback.current(); 5 }, delay); 6 return () => clearTimeout(id); 7}, [delay]);
このuseEffectは、delayが変更されたときにのみ実行されるように、依存配列に[delay]が指定されている。
まず、if (delay == null) return;という条件で、delayがnullの場合にはタイマーを設定せずに処理を終了する。これは、タイマーを一時的に停止したい場合などに利用できる便利な機能だ。
delayがnullでなければ、setTimeoutを呼び出し、指定されたdelayミリ秒後にsavedCallback.current()を実行するように設定する。setTimeoutは、設定されたタイマーを一意に識別するためのIDを返すため、それをid変数に保存している。
このuseEffectの非常に重要なポイントは、return () => clearTimeout(id);という部分だ。これは「クリーンアップ関数」と呼ばれ、useEffectが再実行される前や、コンポーネントが画面から削除(アンマウント)される際に実行される。このクリーンアップ関数内でclearTimeout(id)を呼び出すことで、前に設定されたタイマーを確実にキャンセルする。これにより、delayが変更されるたびに古いタイマーが適切にクリアされ、新しいタイマーが設定されるため、不要なタイマーが残り続けることを防ぎ、常に最新のdelay設定で動作するようになる。
この二つのuseEffectを組み合わせることで、callbackの変更ではタイマーをリセットせず、delayの変更ではタイマーをリセットして再設定するという、要件通りのuseTimeoutカスタムフックが完成する。
記事の最後には、このuseTimeoutフックを実際に利用するAppコンポーネントの例が示されている。
1export function App() { 2 const [count, setCount] = useState(0); 3 const [delay, setDelay] = useState(3000); 4 5 useTimeout(() => { 6 console.log("Timeout fired:", count); 7 setCount((c) => c + 1); 8 }, delay); 9 10 return ( 11 <div> 12 <p> {count} </p> 13 <button onClick={() => setDelay(1000)}> Set delay to 1s </button> 14 <button onClick={() => setDelay(3000)}> Set delay to 3s </button> 15 </div> 16 ); 17}
このAppコンポーネントでは、useStateフックを使ってcount(表示する数値)とdelay(タイマーの遅延時間)という状態を管理している。
useTimeoutフックは、countを1秒または3秒ごとにインクリメントする処理をcallbackとして、現在のdelay値を渡して呼び出されている。
画面には現在のcountが表示され、二つのボタンがある。これらのボタンをクリックすると、setDelay関数によってdelayの状態が1000ミリ秒(1秒)または3000ミリ秒(3秒)に変更される。delayが変更されると、先に説明したuseTimeoutフックの二つ目のuseEffectが反応し、既存のタイマーが解除されて、新しいdelay時間でタイマーが再設定される。これにより、ユーザーがボタンをクリックしてdelayを変更すると、すぐにその新しい遅延時間でcountが更新されるようになる。
このように、useRefとuseEffectを適切に組み合わせることで、Reactのコンポーネント内で非同期処理であるsetTimeoutをより柔軟かつ安全に制御できるカスタムフックを作成できる。システムエンジニアを目指す上で、このような再利用可能でロジックをカプセル化するカスタムフックの考え方は非常に重要だ。