更新日
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 の勉強になりました!参考になれば幸いです!もっといい書き方があればぜひ教えてくださいー!