Reactで作るアナログ時計

以前よりReactの勉強がてら、1時間を40分で区切った36時間時計というのをモクモクと作っています。その中でReact Hooksの使い方などを学んでいるので、今回はよくあるアナログ時計を作りながら勉強メモとして残しておきます。
こんな感じのアナログ時計作ってみた
よくある時計ですね!個人的にはデジタル時計派なのですが、意外と需要があるようなので作ってみることに。ちなみにこれは別にReactを使わなくても、素のJavaScript(Vanilla.js)で作成可能です!が、上記36時間時計を作る中でReactコンポーネントとして必要だったので今回はReactで挑戦しました。
CSSでベースとなるアナログ時計作り
アナログ時計自体は空の div
にサイズや position
の設定をして作成。時計の角度は rotate
プロパティーを使いますが、CSSカスタムプロパティーで初期値を 0 にしておきます。この角度を後ほど動的に変更して、時計の針を動かします。
JavaScript
import "./styles.css"; const App = () => { return ( <div className="clock"> <div className="h-hand"></div> <div className="m-hand"></div> <div className="s-hand"></div> </div> ); }; export default App;
CSS
:root { --degHour: 0; --degMin: 0; --degSec: 0; } .clock { width: 500px; height: 500px; background: #ddf6ff; position: relative; border-radius: 50%; margin: auto; } .h-hand, .m-hand, .s-hand { position: absolute; transform-origin: bottom; border-radius: 40%; } .h-hand { width: 16px; height: 160px; background: #999; top: calc(50% - 160px); left: calc(50% - 8px); rotate: var(--degHour); } .m-hand { width: 10px; height: 220px; background: #999; top: calc(50% - 220px); left: calc(50% - 5px); rotate: var(--degMin); } .s-hand { width: 4px; height: 200px; background: #0bd; top: calc(50% - 200px); left: calc(50% - 2px); rotate: var(--degSec); } .s-hand::after { border-radius: 50%; display: block; content: ''; width: 30px; height: 30px; background: #0bd; position: absolute; bottom: -15px; left: -15px; }
0時0分0秒の状態のアナログ時計完成。
今の時刻を表示させる
React Hooks(フック)は、Reactであらかじめ用意されている便利機能たちです。いろんな種類があるのですが、今回は useState
と useEffect
を使ってみます。これらを使うために、まずはJavaScriptの一行目に以下のコードを入れて利用できる状態にしておきます。
import { useEffect, useState } from "react";
このうちの useState
フックを使うと、コンポーネントの中の状態を管理できるようになります。状態に変化があったら再レンダリングします。初期値として new Date()
で現在の日付や時刻を格納。
const [date, setDate] = useState(new Date());
この段階で {date.getHours()}
を返すと現在の時間が、 {date.getMinutes()}
で分が、 {date.getSeconds()}
で秒が表示されます。
import { useEffect, useState } from "react"; import "./styles.css"; const App = () => { const [date, setDate] = useState(new Date()); return ( <div className="clock"> {date.getHours()}:{date.getMinutes()}:{date.getSeconds()} <div className="h-hand"></div> <div className="m-hand"></div> <div className="s-hand"></div> </div> ); }; export default App;
レンダリングされた時点での時刻が表示されました。
分割代入
現在の時間・分・秒をそれぞれ h
, m
, s
という定数に格納して整理したいので、
const h = date.getHours(); const m = date.getMinutes(); const s = date.getSeconds();
こんな感じで書けばいいのですが、なんかもっとシュッと書けないかなってことで、分割代入にしてみます。分割代入の場合、定数の宣言に []
を使って、配列に格納されているものを順番に当てはめていきます。
const time = [date.getHours(), date.getMinutes(), date.getSeconds()]; const [h, m, s] = time;
これで現在の時刻が h
, m
, s
を呼び出すことで表示できるようになりました。
return ( <div className="clock"> {h}:{m}:{s} <div className="h-hand"></div> <div className="m-hand"></div> <div className="s-hand"></div> </div> );
いい感じですね!
ちなみに、単純にデジタル時計を作りたいだけなら toLocaleTimeString() を使えばいいのですが、今回はアナログ時計として針の角度を出したいので、時・分・秒 それぞれ切り分けて扱えるようにしています。
時計の針の角度を計算
取得した現在の時刻から、それぞれ角度を計算します。時計一周が360°、時間なら12で区切られるので12で、分や秒は60で区切られるので60で割って角度を出します。時間や分は分・秒の角度も加算しています。
const degHour = h * (360 / 12) + m * (360 / 12 / 60); const degMin = m * (360 / 60) + s * (360 / 60 / 60); const degSec = s * (360 / 60);
CSSカスタムプロパティを更新して時計の針の角度を指定
角度が算出されたらCSSのカスタムプロパティでそれぞれ指定します。setProperty
で第一引数にCSSカスタムプロパティ名、第二引数で算出された角度を deg
をつけて指定。
const rootStyle = document.documentElement.style; rootStyle.setProperty("--degHour", `${degHour}deg`); rootStyle.setProperty("--degMin", `${degMin}deg`); rootStyle.setProperty("--degSec", `${degSec}deg`);
現在の時刻がアナログ時計で表示できました!
1秒ごとに更新
この段階では動かず、毎回ページを更新する必要があるので、useState
で設定した date
変数を更新するための setDate
関数を、setInterval
で1秒(=1000ミリ秒)ごとに実行します。
setInterval(() => { setDate(new Date()); }, 1000);
すると…あれ?なんだか秒針の動きが不規則。コンソールで確認してみたところ、ものすごい勢いで増えていくテストメッセージ。
そしてコードを書いていたCodeSandboxがダウン!きゃー!
useEffect を使ってみる
タイマーはコンポーネントがレンダリングされるときに行うので、useEffect
を使ってみることにします。第二引数を設定しておくと、二回目以降のレンダリングの時に、指定した値が変化したときだけ実行するようになります。
useEffect(() => { setInterval(() => { setDate(new Date()); }, 1000); // Get time const time = [date.getHours(), date.getMinutes(), date.getSeconds()]; const [h, m, s] = time; // Get angles const degHour = h * (360 / 12) + m * (360 / 12 / 60); const degMin = m * (360 / 60) + s * (360 / 60 / 60); const degSec = s * (360 / 60); // Set angles to CSS custom property const rootStyle = document.documentElement.style; rootStyle.setProperty("--degHour", `${degHour}deg`); rootStyle.setProperty("--degMin", `${degMin}deg`); rootStyle.setProperty("--degSec", `${degSec}deg`); }, [date]);
あれー!まだものすごい量で呼び出されてるー!そしてまたCodeSandboxがストップしましたよ…。
setInterval
で作成されたタイマーは、clearInterval
関数が呼び出されるまで実行されます。そして useEffect
ではクリーンアップのための機能として、コンポーネントが再レンダリングされる直前などに実行したい処理を、戻り値として指定できるようです。ということで、コンポーネントがアンマウントされると、clearInterval
を使用してタイマーを停止してみます。
useEffect(() => { const timerId = setInterval(() => { setDate(new Date()); }, 1000); // Get time const time = [date.getHours(), date.getMinutes(), date.getSeconds()]; const [h, m, s] = time; // Get angles const degHour = h * (360 / 12) + m * (360 / 12 / 60); const degMin = m * (360 / 60) + s * (360 / 60 / 60); const degSec = s * (360 / 60); // Set angles to CSS custom property const rootStyle = document.documentElement.style; rootStyle.setProperty("--degHour", `${degHour}deg`); rootStyle.setProperty("--degMin", `${degMin}deg`); rootStyle.setProperty("--degSec", `${degSec}deg`); return () => clearInterval(timerId); }, [date]);
これでひとまず問題なく動くようになりました。ひゅー!
完成!
なんとなく使っていたReact Hooksの勉強になりました!参考になれば幸いです!もっといい書き方があればぜひ教えてくださいー!