From 7677622f425a9cbb6e122d249c3f86a0bc4637fb Mon Sep 17 00:00:00 2001 From: Samuel Lachance Date: Wed, 14 Jun 2023 12:29:37 +0200 Subject: [PATCH] Refactor the useEffect --- src/renderer/components/common/Countdown.tsx | 142 ++++++++++++------- 1 file changed, 92 insertions(+), 50 deletions(-) diff --git a/src/renderer/components/common/Countdown.tsx b/src/renderer/components/common/Countdown.tsx index a5c7ee6..c317a15 100644 --- a/src/renderer/components/common/Countdown.tsx +++ b/src/renderer/components/common/Countdown.tsx @@ -5,9 +5,11 @@ import { StyledPopupContainer, } from '@/renderer/components/styles'; import { Button } from '@/renderer/components/common/Button'; -import React, { useEffect, FC, useState, ChangeEvent } from 'react'; +import React, { useEffect, FC, useState, ChangeEvent, useRef } from 'react'; import { LabeledInput } from '@/renderer/components/common/LabeledInput'; +const INTERVAL_MS = 1000; + interface CountdownProps { icon: JSX.Element; labelPopup: string; @@ -15,6 +17,9 @@ interface CountdownProps { onStartClick?: (duration: number) => void; onStopClick?: () => void; sideElement?: JSX.Element; + /** + * If true, the timer will be stopped. + */ isNowStopCountdownTimer?: boolean; } @@ -27,74 +32,111 @@ export const Countdown: FC = ({ sideElement, isNowStopCountdownTimer, }) => { - const timeDisplayDefault = '00:00'; + const intervalRef = useRef>(); const [duration, setDuration] = useState(durationDefault); - const [timeRemaining, setTimeRemaining] = useState(0); - const [isTimerActive, setIsTimerActive] = useState(false); - const [timerDisplay, setTimerDisplay] = useState(timeDisplayDefault); - const [countDownDate, setCountDownDate] = useState(Date.now()); + const [timerDisplay, setTimerDisplay] = useState('00:00'); + const countDownDate = useRef(0); - const updateDuration = (e: ChangeEvent) => { - setDuration(Number(e.target.value)); + + const getTimeRemaining = () => { + return countDownDate.current - Date.now(); + }; + + useEffect(() => { + if (isNowStopCountdownTimer) { + stopTimer(); + updateTimerDisplay(); + } + }, [isNowStopCountdownTimer]); + + /** + * Parses a time in milliseconds to a string in the format mm:ss + * + * @param total - time in milliseconds + */ + const formatTime = (total: number): string => { + const minutes = Math.floor((total % (1000 * 60 * 60)) / (1000 * 60)); + const seconds = Math.floor((total % (1000 * 60)) / 1000); + + const minutesDiplay = minutes.toString().padStart(2, '0'); + const secondsDiplay = seconds.toString().padStart(2, '0'); + + return `${minutesDiplay}:${secondsDiplay}`; }; const startTimer = () => { - setIsTimerActive(false); - setCountDownDate(Date.now() + duration * 60 * 1000); - setIsTimerActive(true); + if (intervalRef.current !== undefined) { + stopTimer(); + } + countDownDate.current = Date.now() + duration * 60 * 1000; + intervalRef.current = setInterval(handleTimerTick, INTERVAL_MS); if (onStartClick !== undefined) { onStartClick(duration); } }; const stopTimer = () => { - setIsTimerActive(false); + clearInterval(intervalRef.current); + intervalRef.current = undefined; + countDownDate.current = 0; if (onStopClick !== undefined) { onStopClick(); } }; - const isShowTimerDisplay = () => { - return timerDisplay !== '00:00'; + /** + * Updates the timer display. + * If the timer is negative, it will be set to 0. + */ + const updateTimerDisplay = () => { + let time = getTimeRemaining(); + if (time < 0) time = 0; + const timeDisplay = formatTime(time); + setTimerDisplay(timeDisplay); }; - useEffect(() => { - let interval: ReturnType | undefined; - const intervalMs = 1000; - if (isTimerActive && !isNowStopCountdownTimer) { - interval = setInterval(() => { - setTimerDisplay(getTimeRemaining()); - setTimeRemaining(timeRemaining - intervalMs); - }, intervalMs); - } else if ( - isNowStopCountdownTimer || - (!isTimerActive && timeRemaining !== 0) - ) { - if (interval !== undefined) { - clearInterval(interval); - } - setTimerDisplay(timeDisplayDefault); - setTimerDisplay(timeDisplayDefault); - setTimeRemaining(0); - } + const isTimerActive = () => { + return countDownDate.current > Date.now(); + }; - const getTimeRemaining = () => { - const total = countDownDate - Date.now(); + /** + * This function is called when the user clicks the start button. + */ + const handleStartButtonClick = () => { + startTimer(); + handleTimerTick(); + }; - const minutes = Math.floor((total % (1000 * 60 * 60)) / (1000 * 60)); - const seconds = Math.floor((total % (1000 * 60)) / 1000); + /** + * This function is called when the user clicks the stop button. + */ + const handleStopButtonClick = () => { + stopTimer(); + updateTimerDisplay(); + }; - const minutesDiplay = minutes.toString().padStart(2, '0'); - const secondsDiplay = seconds.toString().padStart(2, '0'); - if (total < 0) { - setIsTimerActive(false); - } - return `${minutesDiplay}:${secondsDiplay}`; - }; - return () => clearInterval(interval); - }, [isTimerActive, isNowStopCountdownTimer, countDownDate, timeRemaining]); + /** + * This function is called when the user changes the duration. + * @param e - the event + */ + const handleDurationChange = (e: ChangeEvent) => { + setDuration(Number(e.target.value)); + }; + + /** + * This function is called every second by the timer. + */ + const handleTimerTick = () => { + const time = getTimeRemaining(); + if (time <= 0) { + countDownDate.current = 0; + stopTimer(); + } + + updateTimerDisplay(); + }; return ( @@ -102,7 +144,7 @@ export const Countdown: FC = ({ - {isShowTimerDisplay() && timerDisplay} + {isTimerActive() && timerDisplay} {icon} } @@ -118,18 +160,18 @@ export const Countdown: FC = ({ label={`Duration of ${labelPopup} (min)`} value={duration.toString()} type="number" - onChange={updateDuration} + onChange={handleDurationChange} />