3
\$\begingroup\$

I am new to ReactJS and I am trying to write a component which emulates an analog clock. The second, minutes and hour div elements are transformed every second using an interval function. However, I am wondering if there is a better way of transforming the styles for them in the setDate() function?

It seems that what I have written directly modifies the DOM rather than using the Virtual DOM that React provides.

import React from 'react';

class Clock extends React.Component {
    constructor(props) {
        super(props);

        this.state = {
            timer: setInterval(this.setDate, 1000)
        }
    }

    componentWillUnmount() {
        clearInterval(this.state.timer);
    }

    setDate() {
        const secondHand = document.querySelector('.second-hand');
        const minutesHand = document.querySelector('.min-hand');
        const hourHand = document.querySelector('.hour-hand');

        const now = new Date();
        const second = now.getSeconds();
        const minute = now.getMinutes();
        const hour = now.getHours();

        const secondDegrees = (second/60) * 360 + 90;
        const minutesDegrees = (minute/60) * 360 + 90;
        const hourDegrees = (hour/12) * 360 + 90;

        secondHand.style.transform = `rotate(${secondDegrees}deg)`;
        minutesHand.style.transform = `rotate(${minutesDegrees}deg)`;
        hourHand.style.transform = `rotate(${hourDegrees}deg)`;
    }

    render() {
        return (
            <div className="col-md-6">
                <div className="clock">
                    <div className="clock-face">
                        <div className="hand hour-hand"></div>
                        <div className="hand min-hand"></div>
                        <div className="hand second-hand"></div>
                    </div>
                </div>
            </div>
        )
    }
}

export default Clock;
\$\endgroup\$
2
\$\begingroup\$

Yes - whenever the DOM is being queried (e.g. calls to document.querySelector(), etc.), that might likely be a sign that the Virtual DOM is not being utilized properly.

Use the React component state for storing values for degree values

The styles can be added inline such that the degree values of the transform-rotate styles reference values in the state:

<div className="hand hour-hand" style={{transform: `rotate(${this.state.hourDegrees}deg)`}}></div>
<div className="hand min-hand" style={{transform: `rotate(${this.state.minuteDegrees}deg)`}}></div>
<div className="hand second-hand" style={{transform: `rotate(${this.state.secondDegrees}deg)`}}></div>

To set those values in the state, update setDate() to call setState() with those values:

setDate() {
    const now = new Date();
    const second = now.getSeconds();
    const minute = now.getMinutes();
    const hour = now.getHours();
    this.setState({
        secondDegrees:  (second/60) * 360 + 90,
        minutesDegrees: (minute/60) * 360 + 90,
        hourDegrees: (hour/12) * 360 + 90
    });
}

And in order for the closure to reference this of the component properly in the interval callback, it can be bound using Function.bind()- use that when calling setInterval():

constructor(props) {
    super(props);
    this.state = {
        timer: setInterval(this.setDate.bind(this), 1000)
    }
}

The style attributes could also be bound to plain-old Javascript objects- this would be useful in case there were multiple in-line styles to attach. For example:

render() {
    const hourStyle = {
        transform: `rotate(${this.state.hourDegrees}deg)`
    };
    return (
        <div className="col-md-6">a
            <div className="clock">
                <div className="clock-face">
                    <div className="hand hour-hand" style={hourStyle}></div>

Click here for a demonstration in JSFiddle, or expand the snippet below.

var logged = false;
class Clock extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      timer: setInterval(this.setDate.bind(this), 1000),
      secondDegrees: 0,
      minuteDegress: 0,
      hourDegrees: 0
    }
  }
  componentWillUnmount() {
    clearInterval(this.state.timer);
  }
  setDate() {

    const now = new Date();
    const second = now.getSeconds();
    const minute = now.getMinutes();
    const hour = now.getHours();

    const secondDegrees = (second / 60) * 360 + 90;
    const minutesDegrees = (minute / 60) * 360 + 90;
    const hourDegrees = (hour / 12) * 360 + 90;;
    this.setState({
      secondDegrees: (second / 60) * 360 + 90,
      minutesDegrees: (minute / 60) * 360 + 90,
      hourDegrees: (hour / 12) * 360 + 90
    });
  }
  render() {
    const secondStyle = {
      transform: `rotate(${this.state.secondDegrees}deg)`
    };
    const minuteStyle = {
      transform: `rotate(${this.state.minuteDegrees}deg)`
    };
    const hourStyle = {
      transform: `rotate(${this.state.hourDegrees}deg)`
    };
    return ( <
      div className = "col-md-6" >
      <
      div className = "clock" >
      <
      div className = "clock-face" >
      <
      div className = "hand hour-hand"
      style = {
        hourStyle
      } > < /div> <
      div className = "hand min-hand"
      style = {
        minuteStyle
      } > < /div> <
      div className = "hand second-hand"
      style = {
        secondStyle
      } > < /div> <
      /div> <
      /div> <
      /div>
    );
  }
}

ReactDOM.render( <
  Clock / > ,
  document.getElementById('container')
);
/*Matt Litzinger - http://codepen.io/mlitzinger/pen/ruEyz*/

.clock {
  width: 200px;
  height: 200px;
  background: -webkit-radial-gradient(#3b3b3b, #000);
  background: radial-gradient(#3b3b3b, #000);
  box-shadow: inset 0px 0px 30px #777, 0px 2px 18px rgba(0, 0, 0, 0.5), 0px 20px 40px #bbb;
  border: 6px solid #222;
  border-radius: 106px;
  margin: auto;
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
}
.hand {
  background: #fff;
  position: absolute;
  -webkit-transform-origin: 50% 100%;
  -moz-transform-origin: 50% 100%;
  -o-transform-origin: 50% 100%;
  -ms-transform-origin: 50% 100%;
  transform-origin: 50% 100%;
}
.hour-hand {
  width: 4px;
  height: 55px;
  box-shadow: 0px 0px 7px #000;
  top: 45px;
  left: 98px;
}

.min-hand {
  width: 4px;
  height: 80px;
  box-shadow: 0px 0px 4px #000;
  top: 20px;
  left: 98px;
}

.second-hand {
  width: 2px;
  height: 80px;
  background: #bbb;
  box-shadow: 0px 0px 7px #000;
  top: 20px;
  left: 99px;
}

.pin {
  width: 10px;
  height: 10px;
  background: #111;
  border-radius: 10px;
  margin: auto;
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
}

.hour-hand,
.minute-hand,
.second-hand {
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="container">
  <!-- This element's contents will be replaced with your component. -->
</div>

\$\endgroup\$
  • 1
    \$\begingroup\$ BTW, the minute hand doesn't show because the fiddle uses a classname of min-hand instead of minute-hand ... I was going crazy trying to figure it out :) \$\endgroup\$ – Ryan H. Apr 27 '18 at 13:58

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.