Search code examples
javascriptreactjsuse-effect

useEffect won't update state


I'm creating a simple tomato app and got stuck on the timer update logic.

I'm using a useEffect to update a timer, but for some reason after the one second, the timer stop decreasing. It seems it's not getting updated since the console log keep printing the initial values .

here's a testable stackbliz

here's the full component:

import './Tomato.css';
import { useState, useEffect } from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faPlay, faPause, faArrowsRotate, faArrowDown, faArrowUp } from '@fortawesome/free-solid-svg-icons'

function Tomato() {
    
    const [settings, setSettings] = useState({session: 25, break: 5});
    const [timer, setTimer] = useState({minutes: '25', seconds: '00', active: false, isBreak: false});
    

    useEffect(() => {
        let timerInterval;

        if(timer.active) {
            timerInterval = setInterval(() => {
                let seconds = parseInt(timer.seconds);
                let minutes = parseInt(timer.minutes);
                console.log(seconds, ':',minutes)
                if(seconds === 0 && minutes === 0) {
                //  setTimer( {
                //      minutes: settings.isBreak ? settings.session : settings.break, 
                //      seconds: '00', 
                //      active: true, 
                //      isBreak: !settings.isBreak
                //  })
                //  // TODO BIP
                } else if(seconds === 0) {
                    const newTimer = {
                        ...timer,
                        minutes: timer.minutes-1,
                        seconds: '59',
                    }
                    setTimer( newTimer)
                } else {
                    console.log('in')
                    seconds = seconds - 1;
                    // this is not working as expected
                    setTimer( {
                        ...timer,
                        seconds: seconds < 10 ? '0'+seconds : seconds+''
                    })
                }
            }, 1000);
        } else {
            clearInterval(timerInterval);
        }

        return function cleanup() {
            clearInterval(timerInterval);
        };
    }, [timer.active]);


    const toggleTimer = (value) => {
        setTimer({ ...timer, active: value})
    }

    const refreshTimer = () => {
        setTimer({ 
            minutes: settings.session,
            seconds: '00', 
            active: false,
            isBreak: false
        })
    }

    const editSettings =  (type, val) => {
        if(timer.active) return
        const newSettings = { ...settings }
        newSettings[type] +=val;
        if(newSettings[type] > 60) {
            newSettings[type] = 60
        }
        if(newSettings[type] < 1) {
            newSettings[type] = 1
        }
        setSettings(newSettings)
    }

    return (
        <div className="Tomato">
            
            <h2 className="title">Session</h2>
            <div className="session-container">
                <div className='session-value'>{timer.minutes}:{timer.seconds}</div>
                <div className='controls-container'>
                    <span className='icon-container'>
                        <FontAwesomeIcon onClick={()=>{ toggleTimer(true)}} icon={faPlay} />
                    </span>
                    <span className='icon-container'>
                        <FontAwesomeIcon onClick={()=>{ toggleTimer(false)}} icon={faPause} />
                    </span>
                    <span className='icon-container'>
                        <FontAwesomeIcon onClick={()=>{ refreshTimer()}} icon={faArrowsRotate} />
                    </span>
                </div>
            </div>

            <h2 className="title">Session Length</h2>
            <div className="setting-container">
                <span className='icon-container' onClick={()=> editSettings('session', -1)}>
                    <FontAwesomeIcon icon={faArrowDown} />
                </span>
                <div className="setting-value">{settings.session}</div>
                <span className='icon-container' onClick={()=> editSettings('session', 1)}>
                    <FontAwesomeIcon icon={faArrowUp} />
                </span>
            </div>
            
            <h2 className="title">Break Length</h2>
            <div className="setting-container">
                <span className='icon-container' onClick={()=> editSettings('break', -1)}>
                    <FontAwesomeIcon icon={faArrowDown} />
                </span>
                <div className="setting-value">{settings.break}</div>
                <span className='icon-container' onClick={()=> editSettings('break', +1)}>
                    <FontAwesomeIcon icon={faArrowUp} />
                </span>
            </div>
            
        </div>
        );
    }
    
    export default Tomato;
    

Solution

  • That's the typical stale state issue, timer inside the interval is stale since you are not placing it into the effect deps. Just change timer.active to timer in the deps array.

    ...
      return function cleanup() {
                clearInterval(timerInterval);
            };
        }, [timer]);
    

    If you want to know more about this specific issue: https://overreacted.io/making-setinterval-declarative-with-react-hooks/