logo
コーディング

更新日

Reactで作るアナログ時計

以前よりReactの勉強がてら、1時間を40分で区切った36時間時計というのをモクモクと作っています。その中でReact Hooksの使い方などを学んでいるので、今回はよくあるアナログ時計を作りながら勉強メモとして残しておきます。

36 時間時計

こんな感じのアナログ時計作ってみた

よくある時計ですね!個人的にはデジタル時計派なのですが、意外と需要があるようなので作ってみることに。ちなみにこれは別に 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 であらかじめ用意されている便利機能たちです。いろんな種類があるのですが、今回は useStateuseEffect を使ってみます。これらを使うために、まずは 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 の勉強になりました!参考になれば幸いです!もっといい書き方があればぜひ教えてくださいー!