NoobieProgrammer
NoobieProgrammer

Reputation: 90

React having troubles creating a ticking clock that can change time manually

I'm trying to create a clock that will tick every second but also allow the user to click a button to move the clock forward an hour.

    const addTime = (date, time) => new Date(date.getTime() + time)
    
    const Clock = () => {
        const [timeDate, setTimeDate] = useState(addTime(new Date(), SECOND_IN_MS));

        const updateTimeInfo = (addedTime) => {
            setTimeDate(addTime(timeDate, addedTime);
            //other info omitted, won't affect the code
        }

        useEffect(() => {
            let tickClock = setInterval(() => {
               updateTimeInfo(SECOND_IN_MS)
            }, SECOND_IN_MS);

            return () => clearInterval(tickClock);
        }, [timeDate])
    
      
        return (
            <span>{timeDate.toLocaleTimeString([], { hour: '2-digit', hour12: true, minute: '2-digit' });</span>
            <button type="button" onClick={() => updateTimeInfo(HOUR_IN_MS)}>{"Add 1 hour"}</button>
        )
    }

Now the issue is because whenever I update the my clock to new a time, the useEffect is triggered and the clock is going to re-establish the interval to update every second causing the clock to not update the seconds until 1 second has passed.

I would like the clock to always tick 1 second but also allow the user to fast forward 1 hour at the same time.

Is there anyway possible to make this possible?

Upvotes: 1

Views: 332

Answers (1)

Drew Reese
Drew Reese

Reputation: 203542

Remove the timeDate dependency from the useEffect hook and use a functional state update in updateTimeInfo.

  1. Setup interval once when the component mounts, return cleanup function to clear interval.

    useEffect(() => {
      const tickClock = setInterval(() => {
        updateTimeInfo(SECOND_IN_MS);
      }, SECOND_IN_MS);
    
      return () => clearInterval(tickClock);
    }, []);
    
  2. The functional state update allows each enqueued timeDate state update to correctly update from the previous state.

    const updateTimeInfo = (addedTime) => {
      setTimeDate((timeDate) => addTime(timeDate, addedTime));
      //other info omitted, won't affect the code
    };
    

How it works:

The interval runs updateTimeInfo once per second. The button is simply also invoking updateTimeInfo but in an asynchronous way, i.e. whenever the +1 hour button is clicked. These two should occur independently from each other.

Without the functional state update then only the initial timeDate state value is closed over in the effect and time will only update each time from the initial state value.

Code

const SECOND_IN_MS = 1000;
const HOUR_IN_MS = 1000 * 60 * 60;

const addTime = (date, time) => new Date(date.getTime() + time);

const Clock = () => {
  const [timeDate, setTimeDate] = useState(addTime(new Date(), SECOND_IN_MS));

  const updateTimeInfo = (addedTime) => {
    setTimeDate((timeDate) => addTime(timeDate, addedTime));
    //other info omitted, won't affect the code
  };

  useEffect(() => {
    const tickClock = setInterval(() => {
      updateTimeInfo(SECOND_IN_MS);
    }, SECOND_IN_MS);

    return () => clearInterval(tickClock);
  }, []);

  return (
    <>
      <span>
        {timeDate.toLocaleTimeString([], {
          hour: "2-digit",
          hour12: true,
          minute: "2-digit",
          second: "2-digit"
        })}
      </span>
      <button type="button" onClick={() => updateTimeInfo(HOUR_IN_MS)}>
        +1 hour
      </button>
      <button type="button" onClick={() => updateTimeInfo(-HOUR_IN_MS)}>
        -1 hour
      </button>
    </>
  );
};

Edit react-having-troubles-creating-a-ticking-clock-that-can-change-time-manually

Upvotes: 2

Related Questions