LP とかでよく出てくるやつ。個人的にはあまり好きじゃないけど、作るケースが割とあるので。
Window.requestAnimationFrame
The frequency of calls to the callback function will generally match the display refresh rate. The most common refresh rate is 60hz, (60 cycles/frames per second), though 75hz, 120hz, and 144hz are also widely used. requestAnimationFrame() calls are paused in most browsers when running in background tabs or hidden <iframe>s, in order to improve performance and battery life.
Warning: Be sure always to use the first argument (or some other method for getting the current time) to calculate how much the animation will progress in a frame — otherwise, the animation will run faster on high refresh-rate screens. For ways to do that, see the examples below.
cf. Window: requestAnimationFrame() method - Web APIs | MDN
Window.requestAnimationFrame
は画面のリフレッシュレートのタイミングで実行されるので setInterval
や setTimeout
と違いレンダリングを邪魔しないらしく、バックグランドタブや非表示のタブでは実行が一時停止されるので常に実行しておく処理ならこちらのほうがパフォーマンスが良い
リフレッシュレートのタイミングで実行されるので、実行される間隔はディスプレイ依存となる。
リフレッシュレートが60hz なら 1秒に60回、120hzなら 1秒に120回実行される。一定の間隔で実行させるにはアニメーションの経過時間の差を用いるなどの工夫が必要
React で requestAnimationFrame を使ったタイマーを作る
useEffect でループ処理を呼び出してタイマーの状態を更新。コンポーネントがアンマウントされた際に cancelAnimationFrame
でループ処理を止めれば良さそう
import { useState, useEffect, useRef } from 'react'; const App: FC = () => { const [count, setCount] = useState<number>(0); // requrstID を保持させる const reqIdRef = useRef<number>(0); const loopStart = useCallback(() => { let start = 0; const loop = (timestamp?: number) => { const now = Math.round(timestamp); if(start === 0 && now) { start = now; } const progress = (now - start) % 1000; if (progress === 0) { setCount((count) => count += 1); } reqIdRef.current = requestAnimationFrame(loop); } return loop; }, []); const reset = useCallback(() => { cancelAnimationFrame(reqIdRef.current); }, []); useEffect(() => { loopStart()(); return () => reset(); }, []); return ( <div>{count}</div> ); }
Sample
See the Pen React Counter with requestAnimationFrame by KIKIKI (@kikiki_kiki) on CodePen.
カウンドダウンタイマーを作成する
方針
- カウントダウンするターゲットの日時を与える
- ループ内でターゲットの日時と現在の時間のの差を求める
- 差 を 日, 時間, 分 に変換する
- 時間の差が 0 以下のときは
00日00時00分00秒
とする
import { useState, useEffect, useRef } from 'react'; const TARGET_DATE = '2024-08-01'; const App = () => { const targetDate = new Date(TARGET_DATE); const targetTimestamp = targetDate.getTime(); // Invalid Date if ( isNaN(targetTimestamp) ) { return null; } return <CountDownTimer target={targetTimestamp} /> } type Days = number; type Hours = number; type Minutes = number; type Seconds = number; type CounterState = [Days, Hours, Minutes, Seconds]; const LABELS = ['日', '時', '分', '秒'] as const; type CountDownTimerProps = { target: number; } const CountDownTimer: FC<CountDownTimerProps> = ({ target }) => { const [time, setTime] = useState<ReturnType<getTimes>>([0, 0, 0, 0]); const reqIdRef = useRef<number>(0); const loop = useCallback(() => { const now = Date.now(); const distance = target - now; setTime(getTimes(distance)); if (distance < 0) { reset(); return; } reqIdRef.current = requestAnimationFrame(loop); }, [target]); const reset = useCallback(() => { cancelAnimationFrame(reqIdRef.current); }, []); useEffect(() => { loop(); return () => reset(); }, [target]); return ( <div className="countdown"> {time.map((n, i) => ( <div key={i}> <span className="counter">{n.toString().padStart(2, '0')}</span> <span className="label">{LABELS[i]}</span> </div> ))} </div> ); }; // 差の時間から残りの 日, 時間, 分, 秒 を計算して返す const getTimes = (ms: number): CounterState => { if (ms <= 0) { return [0, 0, 0, 0]; } const d = Math.floor(ms / (1000 * 60 * 60 * 24)); const h = Math.floor((ms / (1000 * 60 * 60)) % 24); const m = Math.floor((ms / (1000 * 60)) % 60); const s = Math.floor((ms / 1000) % 60); return [d, h, m, s]; }
Sample
See the Pen React Counter with requestAnimationFrame by KIKIKI (@kikiki_kiki) on CodePen.
サンプル作ろうとしたら 1秒間隔で更新する方が意外とむずくて手間取った
おわり
[参考]
- Window: requestAnimationFrame() method - Web APIs | MDN
- Window: cancelAnimationFrame() method - Web APIs | MDN
- How To Create A Timer With React - DEV Community
- Debouncing events with requestAnimationFrame() for better performance | Go Make Things
- ReactでrequestAnimationFrameを扱う
- [requestAnimationFrame]ループするときのfpsの求め方とfpsに依存しないアニメーション #JavaScript - Qiita
- Canvasだけじゃない!requestAnimationFrameを使ったアニメーション表現 - ICS MEDIA
- requestAnimationFrame でカウントダウンタイマーを表示する | WordPress テーマ DigiPress
- requestAnimationFrameの仕組みと使い方!うまく使ってパフォーマンスを改善しよう! | Tech Blog
- ページ上でずっと動いているsetTimeout、setInterval、requestAnimationFrameを見つけてパフォーマンス改善する | Web Scratch
- JavaScript で Invalid Date を判定する