Skip to content Skip to sidebar Skip to footer

Setstate Does Not Update State Immediately Inside Setinterval

I am trying to build an analog clock with the second hand rotating every second, min hand rotating 6deg every minute and hour hand rotating 6deg every 12 minutes. Here's the codesa

Solution 1:

There's a fundamental design problem here which is that setInterval is the wrong tool for keeping time. setInterval only guarantees that the callback won't run for at least 1000 milliseconds, not that it will run exactly in 1000 milliseconds, so you're going to wind up with drift and skipped times.

I recommend using requestAnimationFrame and the Date library or performance.now to determine when ticks have occurred.

With this set up, you can scale the hour hand proportional to the number of minutes left in the hour with:

(hours + minutes / 60) * 30 + 180

If you want a rougher granularity to the hour hand adjustments, truncate the minutes into 6 distinct chunks:

(hours + floor(minutes / 10) * 10 / 60) * 30 + 180

Doing this mathematically is much less messy than looking up the increment points in a hardcoded array.

Here's a minimal example which you could use to keep accurate time (I'll leave styling to you):

.hand {
  width: 2px;
  height: 40%;
  background-color: black;
  transform-origin: top center;
  position: absolute;
  border-radius: 3px;
  top: 50%;
  left: 50%;
}

.analog-clock {
  position: relative;
  border-radius: 50%;
  border: 1px solid #aaa;
  height: 120px;
  width: 120px;
}
<scripttype="text/babel"defer>const {Fragment, useEffect, useState, useRef} = React;

constClock = () => {
  const [date, setDate] = useState(newDate());
  const requestRef = useRef();
  let prevDate = null;
  
  consttick = () => {
    const now = newDate();
    
    if (prevDate && now.getSeconds() !== prevDate.getSeconds()) {
      setDate(now);
    }
    
    prevDate = now;
    requestRef.current = requestAnimationFrame(tick);
  };
  
  useEffect(() => {
    requestRef.current = requestAnimationFrame(tick);
    return() =>cancelAnimationFrame(requestRef.current);
  }, []);
  
  constpad = n => n.toString().padStart(2, 0);
  
  constcomputeHourDeg = date => 
    (date.getHours() + ~~(date.getMinutes() / 10) * 10 / 60) * 30 + 180
  ;

  return (
    <Fragment><divclassName="analog-clock"><divclassName="hand"style={{transform: `rotate(${6 * date.getSeconds() + 180}deg)`}}
        ></div><divclassName="hand"style={{transform: `rotate(${6 * date.getMinutes() + 180}deg)`}}
        ></div><divclassName="hand"style={{background: "red", 
                  height: "30%",
                  transform: `rotate(${computeHourDeg(date)}deg)`}}
        ></div></div><h3>
        {pad(date.getHours())}:
        {pad(date.getMinutes())}:
        {pad(date.getSeconds())}
      </h3></Fragment>
  );
};

ReactDOM.render(<Clock />, document.body);

</script><scriptsrc="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.26.0/babel.min.js"></script><scriptsrc="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script><scriptsrc="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>

Here's a sped-up version with a mocked Date object to illustrate that it's working correctly:

.hand {
  width: 2px;
  height: 40%;
  background-color: black;
  transform-origin: top center;
  position: absolute;
  border-radius: 3px;
  top: 50%;
  left: 50%;
}

.analog-clock {
  position: relative;
  border-radius: 50%;
  border: 1px solid #aaa;
  height: 120px;
  width: 120px;
}
<scripttype="text/babel"defer>const {Fragment, useEffect, useState, useRef} = React;

const speedMS = 5;

classMockDate {
  static second = 0;
  static minute = 0;
  static hour = 0;
  
  constructor() {
    this.second = MockDate.second;
    this.minute = MockDate.minute;
    this.hour = MockDate.hour;
  }
  
  getSeconds() {
    returnthis.second;
  }
  
  getMinutes() {
    returnthis.minute;
  }
  
  getHours() {
    returnthis.hour || 12;
  }
}

setInterval(() => {
  if (++MockDate.second === 60) {
    MockDate.second = 0;

    if (++MockDate.minute === 60) {
      MockDate.minute = 0;
      MockDate.hour = (MockDate.hour + 1) % 12;
    }
  }
}, speedMS);

constClock = () => {
  const [date, setDate] = useState(newMockDate());
  const requestRef = useRef();
  let prevDate = null;

  consttick = () => {
    const now = newMockDate();

    if (prevDate && now.getSeconds() !== prevDate.getSeconds()) {
      setDate(now);
    }

    prevDate = now;
    requestRef.current = requestAnimationFrame(tick);
  };

  useEffect(() => {
    requestRef.current = requestAnimationFrame(tick);
    return() =>cancelAnimationFrame(requestRef.current);
  }, []);

  constpad = n => n.toString().padStart(2, 0);

  constcomputeHourDeg = date => 
    (date.getHours() + ~~(date.getMinutes() / 10) * 10 / 60) * 30 + 180
  ;

  return (
    <Fragment><divclassName="analog-clock"><divclassName="hand"style={{transform: `rotate(${6 * date.getSeconds() + 180}deg)`}}
        ></div><divclassName="hand"style={{transform: `rotate(${6 * date.getMinutes() + 180}deg)`}}
        ></div><divclassName="hand"style={{background: "red", 
                  height: "30%",
                  transform: `rotate(${computeHourDeg(date)}deg)`}}
        ></div></div><h3>
        {pad(date.getHours())}:
        {pad(date.getMinutes())}:
        {pad(date.getSeconds())}
      </h3></Fragment>
  );
};


ReactDOM.render(<Clock />, document.body);

</script><scriptsrc="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.26.0/babel.min.js"></script><scriptsrc="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script><scriptsrc="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>

Post a Comment for "Setstate Does Not Update State Immediately Inside Setinterval"